const MASK_64 = (1n << 64n) - 1n;
const ROT = [
  0, 1, 62, 28, 27,
  36, 44, 6, 55, 20,
  3, 10, 43, 25, 39,
  41, 45, 15, 21, 8,
  18, 2, 61, 56, 14
];
const RC = [
  0x0000000000000001n,0x0000000000008082n,0x800000000000808an,0x8000000080008000n,
  0x000000000000808bn,0x0000000080000001n,0x8000000080008081n,0x8000000000008009n,
  0x000000000000008an,0x0000000000000088n,0x0000000080008009n,0x000000008000000an,
  0x000000008000808bn,0x800000000000008bn,0x8000000000008089n,0x8000000000008003n,
  0x8000000000008002n,0x8000000000000080n,0x000000000000800an,0x800000008000000an,
  0x8000000080008081n,0x8000000000008080n,0x0000000080000001n,0x8000000080008008n
];

function rotl64(x, n) {
  const shift = BigInt(n % 64);
  if (shift === 0n) return x & MASK_64;
  return ((x << shift) | (x >> (64n - shift))) & MASK_64;
}

function keccakF(state) {
  for (const rc of RC) {
    const c = new Array(5);
    const d = new Array(5);
    for (let x = 0; x < 5; x += 1) {
      c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
    }
    for (let x = 0; x < 5; x += 1) d[x] = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1);
    for (let x = 0; x < 5; x += 1) {
      for (let y = 0; y < 5; y += 1) state[x + 5 * y] = (state[x + 5 * y] ^ d[x]) & MASK_64;
    }

    const b = new Array(25).fill(0n);
    for (let x = 0; x < 5; x += 1) {
      for (let y = 0; y < 5; y += 1) {
        const index = x + 5 * y;
        const nx = y;
        const ny = (2 * x + 3 * y) % 5;
        b[nx + 5 * ny] = rotl64(state[index], ROT[index]);
      }
    }
    for (let x = 0; x < 5; x += 1) {
      for (let y = 0; y < 5; y += 1) {
        state[x + 5 * y] = (b[x + 5 * y] ^ ((~b[(x + 1) % 5 + 5 * y]) & b[(x + 2) % 5 + 5 * y])) & MASK_64;
      }
    }
    state[0] = (state[0] ^ rc) & MASK_64;
  }
}

function absorbBlock(state, block) {
  for (let lane = 0; lane < block.length / 8; lane += 1) {
    let value = 0n;
    for (let byte = 0; byte < 8; byte += 1) value |= BigInt(block[lane * 8 + byte]) << BigInt(byte * 8);
    state[lane] = (state[lane] ^ value) & MASK_64;
  }
  keccakF(state);
}

export function keccak256(input) {
  const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
  const rate = 136;
  const state = new Array(25).fill(0n);
  let offset = 0;
  while (offset + rate <= bytes.length) {
    absorbBlock(state, bytes.subarray(offset, offset + rate));
    offset += rate;
  }
  const finalBlock = Buffer.alloc(rate);
  bytes.copy(finalBlock, 0, offset);
  finalBlock[bytes.length - offset] ^= 0x01; // Keccak domain separator (not SHA3)
  finalBlock[rate - 1] ^= 0x80;
  absorbBlock(state, finalBlock);

  const output = Buffer.alloc(32);
  for (let i = 0; i < output.length; i += 1) {
    const lane = Math.floor(i / 8);
    output[i] = Number((state[lane] >> BigInt((i % 8) * 8)) & 0xffn);
  }
  return output;
}

export function keccak256Hex(input) {
  return keccak256(input).toString('hex');
}
