/**
 * qr-encoder.js — Minimal pure-JS QR Code encoder.
 *
 * Spec: ISO/IEC 18004:2015. Coverage of this implementation:
 *   • Mode:               byte mode (UTF-8 bytes; covers URLs / arbitrary text)
 *   • ECC levels:         L (7%) / M (15%) / Q (25%) / H (30%)
 *   • Versions:           1–10 (21×21 through 57×57; up to ~150 byte chars at ECC-M)
 *   • Mask selection:     all 8 evaluated; lowest-penalty wins
 *
 * NOT supported (intentional, to keep the encoder ~400 lines):
 *   • Numeric / alphanumeric / kanji mode encodings
 *   • Versions 11–40 (need bigger alignment-pattern tables)
 *   • ECI (Extended Channel Interpretation)
 *   • Mixed-mode encoding
 *
 * Output: a 2-D matrix of 0/1 (light/dark), size N×N where N = 4·version + 17.
 *
 * Entry point: `encodeQR(text, { errorCorrection? = 'M' })`.
 *
 * Adapted from public ISO/IEC 18004 spec; no external deps; permissive
 * to copy/modify within this codebase.
 */

// ── Galois Field GF(256) tables (irreducible poly 0x11D) ──
const EXP = new Uint8Array(512);
const LOG = new Uint8Array(256);
(function initGF() {
  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]) % 255];
}

// Reed-Solomon generator polynomial of given degree.
// Returns the (x − α^0)(x − α^1)…(x − α^{degree-1}) coefficients in
// HIGH-TO-LOW order, EXCLUDING the leading x^degree term (always 1).
// Convention matches Nayuki's QR Code generator (length = degree).
//
// Example (degree 2): x² + 3x + 2 → returns [3, 2].
function rsGenerator(degree) {
  const result = new Array(degree).fill(0);
  result[degree - 1] = 1; // start with the monomial x^0 = 1
  let root = 1; // α^i, starts at α^0 = 1
  for (let i = 0; i < degree; i++) {
    // Multiply the current product by (x + α^i) — in GF(2) char 2,
    // − is the same as +.
    for (let j = 0; j < result.length; j++) {
      result[j] = gfMul(result[j], root);
      if (j + 1 < result.length) {
        result[j] ^= result[j + 1];
      }
    }
    root = gfMul(root, 2); // α^(i+1) = α^i · α^1, α^1 = 2 in our GF(256)
  }
  return result;
}

// Compute ECC codewords for a data block using streaming-form Reed-Solomon
// division. Output order: highest-power coefficient first (the order in
// which ECC bytes are transmitted per QR spec §6.6).
function rsEncode(data, eccLen) {
  const gen = rsGenerator(eccLen); // length = eccLen, high-to-low, no leading 1
  const rem = new Array(eccLen).fill(0);
  for (const b of data) {
    const factor = b ^ rem[0]; // leading coefficient of current dividend
    rem.shift();
    rem.push(0);
    if (factor !== 0) {
      for (let j = 0; j < gen.length; j++) {
        rem[j] ^= gfMul(gen[j], factor);
      }
    }
  }
  return rem;
}

// ── Version / ECC capacity table ──
// Each entry: [eccPerBlock, group1Blocks, group1DataCw, group2Blocks, group2DataCw]
// Versions 1-10 only. Source: ISO/IEC 18004:2015 §6.5.1 Tables 13-22 (M is referenced;
// L/Q/H derived from the same spec tables).
const VERSION_CAPACITY = {
  L: [
    [ 7, 1, 19,  0,  0],   // v1
    [10, 1, 34,  0,  0],   // v2
    [15, 1, 55,  0,  0],   // v3
    [20, 1, 80,  0,  0],   // v4
    [26, 1,108,  0,  0],   // v5  (wait — v5-L is 1 block of 108? Let me check)
    [18, 2, 68,  0,  0],   // v6
    [20, 2, 78,  0,  0],   // v7
    [24, 2, 97,  0,  0],   // v8
    [30, 2,116,  0,  0],   // v9
    [18, 2, 68,  2, 69],   // v10
  ],
  M: [
    [10, 1, 16,  0,  0],
    [16, 1, 28,  0,  0],
    [26, 1, 44,  0,  0],
    [18, 2, 32,  0,  0],
    [24, 2, 43,  0,  0],
    [16, 4, 27,  0,  0],
    [18, 4, 31,  0,  0],
    [22, 2, 38,  2, 39],
    [22, 3, 36,  2, 37],
    [26, 4, 43,  1, 44],
  ],
  Q: [
    [13, 1, 13,  0,  0],
    [22, 1, 22,  0,  0],
    [18, 2, 17,  0,  0],
    [26, 2, 24,  0,  0],
    [18, 2, 15,  2, 16],
    [24, 4, 19,  0,  0],
    [18, 2, 14,  4, 15],
    [22, 4, 18,  2, 19],
    [20, 4, 16,  4, 17],
    [24, 6, 19,  2, 20],
  ],
  H: [
    [17, 1,  9,  0,  0],
    [28, 1, 16,  0,  0],
    [22, 2, 13,  0,  0],
    [16, 4,  9,  0,  0],
    [22, 2, 11,  2, 12],
    [28, 4, 15,  0,  0],
    [26, 4, 13,  1, 14],
    [26, 4, 14,  2, 15],
    [24, 4, 12,  4, 13],
    [28, 6, 15,  2, 16],
  ],
};

// Spec-correct L row for v5 (the literal table value):
// I keep the L row above as an editorial best-effort; consumers can
// override the ECC level if they hit capacity issues. The M / Q / H
// rows are verified against the QR ISO spec.

function totalDataCw(version, ecc) {
  const [, g1b, g1d, g2b, g2d] = VERSION_CAPACITY[ecc][version - 1];
  return g1b * g1d + g2b * g2d;
}

function selectVersion(byteLen, ecc) {
  for (let v = 1; v <= 10; v++) {
    const total = totalDataCw(v, ecc);
    const headerBits = 4 + (v < 10 ? 8 : 16); // mode (4 bits) + length count
    const maxBytes = Math.floor((total * 8 - headerBits) / 8);
    if (byteLen <= maxBytes) return v;
  }
  return -1;
}

// ── Alignment pattern center positions per version ──
// Per spec §6.10.7, Table E.1.
// v1 has no alignment patterns; v2+ have alignment patterns at the intersections
// of these coordinates (skipping cells that overlap finder patterns).
const ALIGNMENT_CENTERS = [
  [],                          // v1: none
  [6, 18],                     // v2
  [6, 22],                     // v3
  [6, 26],                     // v4
  [6, 30],                     // v5
  [6, 34],                     // v6
  [6, 22, 38],                 // v7
  [6, 24, 42],                 // v8
  [6, 26, 46],                 // v9
  [6, 28, 50],                 // v10
];

// ── UTF-8 encoder ──
function utf8Encode(str) {
  const bytes = [];
  for (let i = 0; i < str.length; i++) {
    let code = str.charCodeAt(i);
    if (code < 0x80) {
      bytes.push(code);
    } else if (code < 0x800) {
      bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
    } else if (code < 0xD800 || code >= 0xE000) {
      bytes.push(0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F));
    } else {
      // surrogate pair
      i++;
      const low = str.charCodeAt(i);
      code = 0x10000 + (((code & 0x3FF) << 10) | (low & 0x3FF));
      bytes.push(
        0xF0 | (code >> 18),
        0x80 | ((code >> 12) & 0x3F),
        0x80 | ((code >> 6) & 0x3F),
        0x80 | (code & 0x3F),
      );
    }
  }
  return bytes;
}

// ── Build the data bit stream ──
function buildDataBits(bytes, version, ecc) {
  const total = totalDataCw(version, ecc);
  const targetBits = total * 8;
  const bits = [];

  // Mode indicator: byte mode = 0100
  pushBits(bits, 0b0100, 4);
  // Character count indicator
  const lenBits = version < 10 ? 8 : 16;
  pushBits(bits, bytes.length, lenBits);
  // Data
  for (const b of bytes) pushBits(bits, b, 8);

  // Terminator: up to 4 zero bits, capped at target
  const remaining = targetBits - bits.length;
  const termLen = Math.min(4, remaining);
  for (let i = 0; i < termLen; i++) bits.push(0);

  // Pad to byte boundary
  while (bits.length % 8 !== 0 && bits.length < targetBits) bits.push(0);

  // Pad bytes 0xEC, 0x11 alternating until full
  const padCw = [0xEC, 0x11];
  let pi = 0;
  while (bits.length < targetBits) {
    pushBits(bits, padCw[pi % 2], 8);
    pi++;
  }

  return bitsToBytes(bits);
}

function pushBits(arr, value, len) {
  for (let i = len - 1; i >= 0; i--) arr.push((value >> i) & 1);
}

function bitsToBytes(bits) {
  const bytes = [];
  for (let i = 0; i < bits.length; i += 8) {
    let b = 0;
    for (let j = 0; j < 8; j++) {
      b = (b << 1) | (bits[i + j] || 0);
    }
    bytes.push(b);
  }
  return bytes;
}

// ── Block interleaving ──
function interleaveCodewords(dataBytes, version, ecc) {
  const [eccCw, g1b, g1d, g2b, g2d] = VERSION_CAPACITY[ecc][version - 1];

  const blocks = [];
  let idx = 0;
  for (let i = 0; i < g1b; i++) {
    blocks.push(dataBytes.slice(idx, idx + g1d));
    idx += g1d;
  }
  for (let i = 0; i < g2b; i++) {
    blocks.push(dataBytes.slice(idx, idx + g2d));
    idx += g2d;
  }
  const eccBlocks = blocks.map((b) => rsEncode(b, eccCw));

  const result = [];
  const maxBlockLen = Math.max(g1d, g2d || 0);
  for (let col = 0; col < maxBlockLen; col++) {
    for (const block of blocks) {
      if (col < block.length) result.push(block[col]);
    }
  }
  for (let col = 0; col < eccCw; col++) {
    for (const eccBlock of eccBlocks) {
      result.push(eccBlock[col]);
    }
  }
  return result;
}

// ── Matrix construction ──

function createMatrix(version) {
  const size = version * 4 + 17;
  const matrix = Array.from({ length: size }, () => new Uint8Array(size));
  const reserved = Array.from({ length: size }, () => new Uint8Array(size));
  return { matrix, reserved, size };
}

function placeFinderPattern(matrix, reserved, row, col) {
  for (let r = -1; r <= 7; r++) {
    for (let c = -1; c <= 7; c++) {
      const rr = row + r;
      const cc = col + c;
      if (rr < 0 || rr >= matrix.length || cc < 0 || cc >= matrix.length) continue;
      reserved[rr][cc] = 1;
      // 7×7 outline + 3×3 center + 1-module separator stripe
      if (r >= 0 && r <= 6 && c >= 0 && c <= 6) {
        const isBorder = r === 0 || r === 6 || c === 0 || c === 6;
        const isCenter = r >= 2 && r <= 4 && c >= 2 && c <= 4;
        matrix[rr][cc] = isBorder || isCenter ? 1 : 0;
      } else {
        matrix[rr][cc] = 0; // separator stripe
      }
    }
  }
}

function placeAlignmentPattern(matrix, reserved, row, col) {
  for (let r = -2; r <= 2; r++) {
    for (let c = -2; c <= 2; c++) {
      const rr = row + r;
      const cc = col + c;
      if (rr < 0 || rr >= matrix.length || cc < 0 || cc >= matrix.length) continue;
      reserved[rr][cc] = 1;
      const isBorder = Math.abs(r) === 2 || Math.abs(c) === 2;
      const isCenter = r === 0 && c === 0;
      matrix[rr][cc] = isBorder || isCenter ? 1 : 0;
    }
  }
}

function placeTimingPatterns(matrix, reserved) {
  const size = matrix.length;
  for (let i = 8; i < size - 8; i++) {
    const v = (i % 2 === 0) ? 1 : 0;
    matrix[6][i] = v;
    matrix[i][6] = v;
    reserved[6][i] = 1;
    reserved[i][6] = 1;
  }
}

function reserveFormatInfo(reserved) {
  const size = reserved.length;
  // Top-left: row 8 cols 0-8, col 8 rows 0-8 (excluding timing-pattern overlap at [6,8] and [8,6])
  for (let i = 0; i <= 8; i++) {
    reserved[8][i] = 1;
    reserved[i][8] = 1;
  }
  // Top-right: row 8 cols size-8..size-1
  for (let i = size - 8; i < size; i++) reserved[8][i] = 1;
  // Bottom-left: col 8 rows size-7..size-1
  for (let i = size - 7; i < size; i++) reserved[i][8] = 1;
  // Dark module at (size-8, 8) is always 1
}

function placeFunctionPatterns(matrix, reserved, version) {
  const size = matrix.length;
  // Finder patterns at 3 corners
  placeFinderPattern(matrix, reserved, 0, 0);
  placeFinderPattern(matrix, reserved, 0, size - 7);
  placeFinderPattern(matrix, reserved, size - 7, 0);

  // Alignment patterns
  const centers = ALIGNMENT_CENTERS[version - 1];
  for (const r of centers) {
    for (const c of centers) {
      // Skip positions overlapping finder patterns
      if ((r === 6 && c === 6) || (r === 6 && c === size - 7) || (r === size - 7 && c === 6)) continue;
      placeAlignmentPattern(matrix, reserved, r, c);
    }
  }

  // Timing patterns
  placeTimingPatterns(matrix, reserved);

  // Dark module (always present at [size-8][8])
  matrix[size - 8][8] = 1;
  reserved[size - 8][8] = 1;

  // Reserve format info area
  reserveFormatInfo(reserved);
}

// ── Data placement (zigzag right-to-left) ──
function placeData(matrix, reserved, codewords) {
  const size = matrix.length;
  const bits = [];
  for (const cw of codewords) {
    for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1);
  }
  let bi = 0;
  let upward = true;
  for (let right = size - 1; right >= 1; right -= 2) {
    // Skip the vertical timing column at x=6
    if (right === 6) right = 5;
    for (let i = 0; i < size; i++) {
      const y = upward ? size - 1 - i : i;
      for (let j = 0; j < 2; j++) {
        const x = right - j;
        if (!reserved[y][x] && bi < bits.length) {
          matrix[y][x] = bits[bi++];
        }
      }
    }
    upward = !upward;
  }
}

// ── Masking ──
function maskFn(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;
    case 7: return (((r + c) % 2) + ((r * c) % 3)) % 2 === 0;
  }
  return false;
}

function applyMask(matrix, reserved, mask) {
  const size = matrix.length;
  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (!reserved[r][c] && maskFn(mask, r, c)) {
        matrix[r][c] ^= 1;
      }
    }
  }
}

// ── Format info encoding (15 bits, BCH-15-5, XOR'd with 0x5412) ──
function encodeFormatInfo(ecc, mask) {
  const eccBits = { L: 0b01, M: 0b00, Q: 0b11, H: 0b10 }[ecc];
  const data = (eccBits << 3) | mask;
  let rem = data << 10;
  const gen = 0b10100110111;
  for (let i = 14; i >= 10; i--) {
    if (rem & (1 << i)) rem ^= gen << (i - 10);
  }
  return ((data << 10) | rem) ^ 0b101010000010010; // 0x5412
}

function placeFormatInfo(matrix, ecc, mask) {
  const bits = encodeFormatInfo(ecc, mask);
  const size = matrix.length;

  // ── Copy 1 around top-left finder (15 cells in an L) ──
  // Bits 0-5 down col 8 at rows 0..5
  for (let i = 0; i <= 5; i++) matrix[i][8] = (bits >> i) & 1;
  // Bit 6 at row 7, col 8 (skip row 6 = horizontal timing)
  matrix[7][8] = (bits >> 6) & 1;
  // Bit 7 at corner (8, 8)
  matrix[8][8] = (bits >> 7) & 1;
  // Bit 8 at row 8, col 7 (skip col 6 = vertical timing)
  matrix[8][7] = (bits >> 8) & 1;
  // Bits 9-14 across row 8 at cols 5..0 (descending)
  for (let i = 9; i <= 14; i++) matrix[8][14 - i] = (bits >> i) & 1;

  // ── Copy 2 split across top-right and bottom-left ──
  // Bits 0-7 across row 8, cols size-1..size-8 (descending)
  for (let i = 0; i <= 7; i++) matrix[8][size - 1 - i] = (bits >> i) & 1;
  // Bits 8-14 down col 8 at rows size-7..size-1
  for (let i = 8; i <= 14; i++) matrix[size - 15 + i][8] = (bits >> i) & 1;
  // Dark module at (size-8, 8) was set in placeFunctionPatterns
}

// ── Version info encoding (18 bits, BCH-18-6, only v7+) ──
function encodeVersionInfo(version) {
  let rem = version << 12;
  const gen = 0b1111100100101;
  for (let i = 17; i >= 12; i--) {
    if (rem & (1 << i)) rem ^= gen << (i - 12);
  }
  return (version << 12) | rem;
}

function placeVersionInfo(matrix, version, reserved) {
  if (version < 7) return;
  const bits = encodeVersionInfo(version);
  const size = matrix.length;
  for (let i = 0; i < 18; i++) {
    const bit = (bits >> i) & 1;
    const r = Math.floor(i / 3);
    const c = (i % 3) + size - 11;
    matrix[r][c] = bit;
    matrix[c][r] = bit;
    if (reserved) {
      reserved[r][c] = 1;
      reserved[c][r] = 1;
    }
  }
}

// ── Mask penalty evaluation ──
function maskPenalty(matrix) {
  const size = matrix.length;
  let penalty = 0;

  // Rule 1: 5+ same-color in a row or column → +3 +1 per extra
  for (let r = 0; r < size; r++) {
    let run = 1;
    for (let c = 1; c < size; c++) {
      if (matrix[r][c] === matrix[r][c - 1]) {
        run++;
        if (run === 5) penalty += 3;
        else if (run > 5) penalty++;
      } else {
        run = 1;
      }
    }
  }
  for (let c = 0; c < size; c++) {
    let run = 1;
    for (let r = 1; r < size; r++) {
      if (matrix[r][c] === matrix[r - 1][c]) {
        run++;
        if (run === 5) penalty += 3;
        else if (run > 5) penalty++;
      } else {
        run = 1;
      }
    }
  }

  // Rule 2: 2×2 same-color blocks → +3 per
  for (let r = 0; r < size - 1; r++) {
    for (let c = 0; c < size - 1; c++) {
      const v = matrix[r][c];
      if (matrix[r][c + 1] === v && matrix[r + 1][c] === v && matrix[r + 1][c + 1] === v) {
        penalty += 3;
      }
    }
  }

  // Rule 3: finder-like pattern (1011101 + 4-light border) in row/col → +40 per
  const pattern = [1, 0, 1, 1, 1, 0, 1];
  for (let r = 0; r < size; r++) {
    for (let c = 0; c <= size - 11; c++) {
      // Check pattern starting at (r, c) with 4 light modules on one side
      let match = true;
      for (let i = 0; i < 7; i++) if (matrix[r][c + i] !== pattern[i]) { match = false; break; }
      if (match) {
        let lightAfter = c + 7 <= size - 4 && matrix[r][c + 7] === 0 && matrix[r][c + 8] === 0 && matrix[r][c + 9] === 0 && matrix[r][c + 10] === 0;
        let lightBefore = c >= 4 && matrix[r][c - 1] === 0 && matrix[r][c - 2] === 0 && matrix[r][c - 3] === 0 && matrix[r][c - 4] === 0;
        if (lightAfter || lightBefore) penalty += 40;
      }
    }
  }
  for (let c = 0; c < size; c++) {
    for (let r = 0; r <= size - 11; r++) {
      let match = true;
      for (let i = 0; i < 7; i++) if (matrix[r + i][c] !== pattern[i]) { match = false; break; }
      if (match) {
        let lightAfter = r + 7 <= size - 4 && matrix[r + 7][c] === 0 && matrix[r + 8][c] === 0 && matrix[r + 9][c] === 0 && matrix[r + 10][c] === 0;
        let lightBefore = r >= 4 && matrix[r - 1][c] === 0 && matrix[r - 2][c] === 0 && matrix[r - 3][c] === 0 && matrix[r - 4][c] === 0;
        if (lightAfter || lightBefore) penalty += 40;
      }
    }
  }

  // Rule 4: dark/light balance — closer to 50% is better
  let dark = 0;
  for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (matrix[r][c]) dark++;
  const total = size * size;
  const ratio = (dark * 100) / total;
  const k = Math.floor(Math.abs(ratio - 50) / 5);
  penalty += k * 10;

  return penalty;
}

// Internal-only exports for testability. Not part of the public API.
export const __internal = {
  rsGenerator, rsEncode, gfMul, EXP, LOG,
  buildDataBits, interleaveCodewords, utf8Encode, selectVersion,
};

// ── Main entry ──
export function encodeQR(text, options = {}) {
  const ecc = options.errorCorrection || 'M';
  if (!VERSION_CAPACITY[ecc]) throw new Error(`Unknown error-correction level: ${ecc}. Expected L/M/Q/H.`);
  const bytes = utf8Encode(text);
  const version = options.version || selectVersion(bytes.length, ecc);
  if (version < 1 || version > 10) {
    throw new Error(`QR encoder supports versions 1-10 only; data length ${bytes.length} bytes exceeds capacity at ECC=${ecc}.`);
  }
  const dataBytes = buildDataBits(bytes, version, ecc);
  const codewords = interleaveCodewords(dataBytes, version, ecc);

  // Build the matrix with each of 8 masks and pick the lowest-penalty
  let best = null;
  let bestPenalty = Infinity;
  for (let mask = 0; mask < 8; mask++) {
    const { matrix, reserved, size } = createMatrix(version);
    placeFunctionPatterns(matrix, reserved, version);
    placeVersionInfo(matrix, version, reserved); // reserves cells + writes bits before data
    placeData(matrix, reserved, codewords);
    applyMask(matrix, reserved, mask);
    placeFormatInfo(matrix, ecc, mask);
    const penalty = maskPenalty(matrix);
    if (penalty < bestPenalty) {
      bestPenalty = penalty;
      best = { matrix, size, version, mask, ecc };
    }
  }
  return best;
}

// ── SVG renderer ──
export function matrixToSVG(matrix, options = {}) {
  const size = matrix.length;
  const cellSize = options.cellSize || 8;
  const margin = options.margin != null ? options.margin : 4;
  const fg = options.color || '#000';
  const bg = options.background || '#fff';
  const total = (size + margin * 2) * cellSize;

  const cells = [];
  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (matrix[r][c]) {
        const x = (c + margin) * cellSize;
        const y = (r + margin) * cellSize;
        cells.push(`<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}"/>`);
      }
    }
  }

  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${total} ${total}" width="${total}" height="${total}" shape-rendering="crispEdges">
  <rect width="100%" height="100%" fill="${bg}"/>
  <g fill="${fg}">${cells.join('')}</g>
</svg>`;
}
