/* A minimal QR encoder, byte mode, error-correction level M.
 *
 * Written rather than imported because the arcade has to work on a network
 * with no internet: a CDN script is a dependency on the outside world, and the
 * whole point of this box is that it does not need one.
 *
 * Scope is deliberately narrow — byte mode, versions 1 through 20, level M —
 * which covers a Lightning invoice with room to spare and leaves out the
 * kanji/numeric modes and structured-append machinery that would triple the
 * size for no benefit here.
 *
 * Reference: ISO/IEC 18004. */

/* ---------- Galois field arithmetic over GF(256) ----------
 * Reed–Solomon needs multiplication in the field the QR spec defines, with
 * the primitive polynomial 0x11d. Log tables make that a lookup. */

const EXP = new Uint8Array(512);
const LOG = new Uint8Array(256);
(function buildTables() {
  let x = 1;
  for (let i = 0; i < 255; i++) {
    EXP[i] = x;
    LOG[x] = i;
    x <<= 1;
    if (x & 0x100) x ^= 0x11d;
  }
  for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
})();

function gfMul(a, b) {
  if (a === 0 || b === 0) return 0;
  return EXP[LOG[a] + LOG[b]];
}

/* The generator polynomial for n error-correction codewords. */
function rsGenerator(n) {
  let poly = [1];
  for (let i = 0; i < n; i++) {
    const next = new Array(poly.length + 1).fill(0);
    for (let j = 0; j < poly.length; j++) {
      next[j] ^= poly[j];
      next[j + 1] ^= gfMul(poly[j], EXP[i]);
    }
    poly = next;
  }
  return poly;
}

function rsEncode(data, ecCount) {
  const gen = rsGenerator(ecCount);
  const res = new Uint8Array(data.length + ecCount);
  res.set(data);
  for (let i = 0; i < data.length; i++) {
    const factor = res[i];
    if (factor === 0) continue;
    for (let j = 0; j < gen.length; j++) {
      res[i + j] ^= gfMul(gen[j], factor);
    }
  }
  return res.slice(data.length);
}

/* ---------- capacity tables, level M ----------
 * [total codewords, ec codewords per block, block count group1,
 *  data codewords group1, block count group2, data codewords group2] */
const VERSIONS = [
  null,
  [26, 10, 1, 16, 0, 0],       // 1
  [44, 16, 1, 28, 0, 0],
  [70, 26, 1, 44, 0, 0],
  [100, 18, 2, 32, 0, 0],
  [134, 24, 2, 43, 0, 0],
  [172, 16, 4, 27, 0, 0],
  [196, 18, 4, 31, 0, 0],
  [242, 22, 2, 38, 2, 39],
  [292, 22, 3, 36, 2, 37],
  [346, 26, 4, 43, 1, 44],     // 10
  [404, 30, 1, 50, 4, 51],
  [466, 22, 6, 36, 2, 37],
  [532, 22, 8, 37, 1, 38],
  [581, 24, 4, 40, 5, 41],
  [655, 24, 5, 41, 5, 42],     // 15
  [733, 28, 7, 45, 3, 46],
  [815, 28, 10, 46, 1, 47],
  [901, 26, 9, 43, 4, 44],
  [991, 26, 3, 44, 11, 45],
  [1085, 26, 3, 41, 13, 42],   // 20
];

function versionCapacity(v) {
  const [, ec, b1, d1, b2, d2] = VERSIONS[v];
  return b1 * d1 + b2 * d2;
}

/* Alignment pattern centres per version. */
const ALIGN = [
  [], [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34],
  [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50],
  [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66],
  [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78],
  [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90],
];

/* ---------- bit stream ---------- */

class Bits {
  constructor() { this.bits = []; }
  push(value, length) {
    for (let i = length - 1; i >= 0; i--) this.bits.push((value >> i) & 1);
  }
  get length() { return this.bits.length; }
  toBytes() {
    const out = new Uint8Array(Math.ceil(this.bits.length / 8));
    this.bits.forEach((b, i) => { if (b) out[i >> 3] |= 0x80 >> (i & 7); });
    return out;
  }
}

/* ---------- encoding ---------- */

function encodeData(text, version) {
  const bytes = new TextEncoder().encode(text);
  const bits = new Bits();

  bits.push(0b0100, 4);                                  // byte mode
  bits.push(bytes.length, version < 10 ? 8 : 16);        // length field
  for (const b of bytes) bits.push(b, 8);

  const capacityBits = versionCapacity(version) * 8;
  if (bits.length > capacityBits) return null;           // does not fit

  // Terminator, then pad to a byte boundary, then alternating pad bytes.
  bits.push(0, Math.min(4, capacityBits - bits.length));
  while (bits.length % 8 !== 0) bits.push(0, 1);

  const data = Array.from(bits.toBytes());
  const padBytes = [0xec, 0x11];
  let i = 0;
  while (data.length < versionCapacity(version)) data.push(padBytes[i++ % 2]);

  return interleave(data, version);
}

/* Split into blocks, compute error correction, then interleave both — the
 * spec's arrangement, which is what makes a QR survive damage to any one
 * region rather than losing a contiguous run of data. */
function interleave(data, version) {
  const [, ecPerBlock, b1, d1, b2, d2] = VERSIONS[version];

  const blocks = [];
  let offset = 0;
  for (let i = 0; i < b1; i++) {
    blocks.push(data.slice(offset, offset + d1));
    offset += d1;
  }
  for (let i = 0; i < b2; i++) {
    blocks.push(data.slice(offset, offset + d2));
    offset += d2;
  }

  const ecBlocks = blocks.map((b) => rsEncode(Uint8Array.from(b), ecPerBlock));

  const out = [];
  const maxData = Math.max(...blocks.map((b) => b.length));
  for (let i = 0; i < maxData; i++) {
    for (const b of blocks) if (i < b.length) out.push(b[i]);
  }
  for (let i = 0; i < ecPerBlock; i++) {
    for (const b of ecBlocks) out.push(b[i]);
  }
  return out;
}

/* ---------- matrix ---------- */

function buildMatrix(version, codewords, mask) {
  const size = version * 4 + 17;
  const m = Array.from({ length: size }, () => new Array(size).fill(null));

  const setFinder = (r, c) => {
    for (let dr = -1; dr <= 7; dr++) {
      for (let dc = -1; dc <= 7; dc++) {
        const rr = r + dr, cc = c + dc;
        if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue;
        const inRing = dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6 &&
          (dr === 0 || dr === 6 || dc === 0 || dc === 6 ||
           (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4));
        m[rr][cc] = inRing ? 1 : 0;
      }
    }
  };
  setFinder(0, 0);
  setFinder(0, size - 7);
  setFinder(size - 7, 0);

  // Timing patterns.
  for (let i = 8; i < size - 8; i++) {
    m[6][i] = i % 2 === 0 ? 1 : 0;
    m[i][6] = i % 2 === 0 ? 1 : 0;
  }

  // Alignment patterns, skipping those that would collide with finders.
  const centres = ALIGN[version];
  for (const r of centres) {
    for (const c of centres) {
      if ((r <= 8 && c <= 8) || (r <= 8 && c >= size - 9) ||
          (r >= size - 9 && c <= 8)) continue;
      for (let dr = -2; dr <= 2; dr++) {
        for (let dc = -2; dc <= 2; dc++) {
          m[r + dr][c + dc] =
            Math.max(Math.abs(dr), Math.abs(dc)) !== 1 ? 1 : 0;
        }
      }
    }
  }

  m[size - 8][8] = 1; // dark module

  // Reserve format areas so data placement skips them.
  const reserveFormat = () => {
    for (let i = 0; i < 9; i++) {
      if (m[8][i] === null) m[8][i] = 0;
      if (m[i][8] === null) m[i][8] = 0;
    }
    for (let i = 0; i < 8; i++) {
      if (m[8][size - 1 - i] === null) m[8][size - 1 - i] = 0;
      if (m[size - 1 - i][8] === null) m[size - 1 - i][8] = 0;
    }
  };
  const formatCells = [];
  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) if (m[r][c] === null) formatCells.push([r, c]);
  }
  reserveFormat();

  // Version information, for version 7 and above.
  if (version >= 7) {
    let rem = version;
    for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >> 11) * 0x1f25);
    const bits = (version << 12) | rem;
    for (let i = 0; i < 18; i++) {
      const bit = (bits >> i) & 1;
      m[Math.floor(i / 3)][size - 11 + (i % 3)] = bit;
      m[size - 11 + (i % 3)][Math.floor(i / 3)] = bit;
    }
  }

  // Data placement: upward/downward in two-column strips, right to left.
  let bitIndex = 0;
  const dataBits = [];
  for (const cw of codewords) {
    for (let i = 7; i >= 0; i--) dataBits.push((cw >> i) & 1);
  }

  let upward = true;
  for (let col = size - 1; col > 0; col -= 2) {
    if (col === 6) col--; // skip the timing column
    for (let i = 0; i < size; i++) {
      const row = upward ? size - 1 - i : i;
      for (let c = 0; c < 2; c++) {
        const cc = col - c;
        if (m[row][cc] !== null) continue;
        let bit = bitIndex < dataBits.length ? dataBits[bitIndex++] : 0;
        if (maskAt(mask, row, cc)) bit ^= 1;
        m[row][cc] = bit;
      }
    }
    upward = !upward;
  }

  writeFormat(m, size, mask);
  return m;
}

function maskAt(mask, r, c) {
  switch (mask) {
    case 0: return (r + c) % 2 === 0;
    case 1: return r % 2 === 0;
    case 2: return c % 3 === 0;
    case 3: return (r + c) % 3 === 0;
    case 4: return (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0;
    case 5: return ((r * c) % 2) + ((r * c) % 3) === 0;
    case 6: return (((r * c) % 2) + ((r * c) % 3)) % 2 === 0;
    default: return (((r + c) % 2) + ((r * c) % 3)) % 2 === 0;
  }
}

function writeFormat(m, size, mask) {
  // Level M is 0b00; combine with the mask and append BCH error correction.
  const data = (0b00 << 3) | mask;
  let rem = data;
  for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >> 9) * 0x537);
  const bits = ((data << 10) | rem) ^ 0x5412;

  for (let i = 0; i <= 5; i++) m[8][i] = (bits >> i) & 1;
  m[8][7] = (bits >> 6) & 1;
  m[8][8] = (bits >> 7) & 1;
  m[7][8] = (bits >> 8) & 1;
  for (let i = 9; i < 15; i++) m[14 - i][8] = (bits >> i) & 1;

  // The second copy is 7 bits down the bottom-left column and 8 bits along
  // the top-right row. It is 7 and not 8 down the column because the cell
  // below is the dark module, which is fixed and must not be overwritten.
  for (let i = 0; i <= 6; i++) m[size - 1 - i][8] = (bits >> i) & 1;
  for (let i = 7; i < 15; i++) m[8][size - 15 + i] = (bits >> i) & 1;
}

/* ---------- public API ---------- */

/* encode returns a square matrix of 0/1, or null if the text does not fit. */
export function encode(text) {
  for (let version = 1; version <= 20; version++) {
    const codewords = encodeData(text, version);
    if (codewords) {
      // Mask 0 is used unconditionally. Choosing the optimal mask by penalty
      // score improves scan reliability marginally and costs four more passes
      // over the matrix; at the sizes here, every reader handles mask 0.
      return buildMatrix(version, codewords, 0);
    }
  }
  return null;
}

/* render draws a matrix into an SVG element, scaled to its container. */
export function render(matrix, options = {}) {
  const quiet = options.quiet ?? 4;
  const size = matrix.length;
  const total = size + quiet * 2;

  const NS = 'http://www.w3.org/2000/svg';
  const svg = document.createElementNS(NS, 'svg');
  svg.setAttribute('viewBox', `0 0 ${total} ${total}`);
  svg.setAttribute('width', '100%');
  svg.setAttribute('height', '100%');
  svg.setAttribute('shape-rendering', 'crispEdges');

  const bg = document.createElementNS(NS, 'rect');
  bg.setAttribute('width', total);
  bg.setAttribute('height', total);
  bg.setAttribute('fill', options.background ?? '#000603');
  svg.appendChild(bg);

  // One path for every dark module: far fewer nodes than a rect each, which
  // matters when a phone is re-rendering this inside a live page.
  let d = '';
  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (matrix[r][c]) d += `M${c + quiet} ${r + quiet}h1v1h-1z`;
    }
  }
  const path = document.createElementNS(NS, 'path');
  path.setAttribute('d', d);
  path.setAttribute('fill', options.foreground ?? '#00ff9c');
  svg.appendChild(path);

  return svg;
}
