/* ============================================================
   PIX — BR Code EMV + QR Code generator (offline, no deps)
   - buildPixPayload(opts)  → string "Copia e Cola"
   - generateQRMatrix(text) → array of array of 0/1 (modules)
   ============================================================ */

(function () {
  /* ---------- BR Code EMV ---------- */

  // CRC16/CCITT-FALSE (polynom 0x1021, init 0xFFFF) — utilisé par Pix
  function crc16(str) {
    let crc = 0xFFFF;
    for (let i = 0; i < str.length; i++) {
      crc ^= str.charCodeAt(i) << 8;
      for (let j = 0; j < 8; j++) {
        crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
        crc &= 0xFFFF;
      }
    }
    return crc.toString(16).toUpperCase().padStart(4, "0");
  }

  function tlv(id, value) {
    const v = String(value);
    const len = v.length.toString().padStart(2, "0");
    return id + len + v;
  }

  function clean(str, maxLen) {
    const s = String(str || "")
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .replace(/[^\x20-\x7E]/g, "")
      .toUpperCase();
    return maxLen ? s.slice(0, maxLen) : s;
  }

  function buildPixPayload(opts) {
    const key = String(opts.key || "").trim();
    const name = clean(opts.name, 25);
    const city = clean(opts.city, 15);
    const amount = opts.amount != null ? Number(opts.amount).toFixed(2) : null;
    const txid = clean(opts.txid || "***", 25).replace(/[^A-Z0-9]/g, "") || "***";
    const description = opts.description ? clean(opts.description, 50) : null;

    let merchantAccount = tlv("00", "BR.GOV.BCB.PIX") + tlv("01", key);
    if (description) merchantAccount += tlv("02", description);
    const mai = tlv("26", merchantAccount);
    const additional = tlv("62", tlv("05", txid));

    let payload = "";
    payload += tlv("00", "01");
    payload += tlv("01", "12");
    payload += mai;
    payload += tlv("52", "0000");
    payload += tlv("53", "986");
    if (amount) payload += tlv("54", amount);
    payload += tlv("58", "BR");
    payload += tlv("59", name);
    payload += tlv("60", city);
    payload += additional;
    payload += "6304";

    const crc = crc16(payload);
    return payload + crc;
  }

  /* ---------- QR Code generator (Reed-Solomon, byte mode, ECC level M) ---------- */
  /* Implémentation minimale, support versions 1-10 (capacité ~213 chars en mode byte ECC-M) */

  // Galois field tables (GF(256) avec primitif 0x011D)
  const EXP = new Array(256);
  const LOG = new Array(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 ^= 0x011D;
    }
    EXP[255] = EXP[0];
  })();

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

  // Génère le polynôme générateur pour `degree` bits de correction
  function rsGenPoly(degree) {
    let poly = [1];
    for (let i = 0; i < degree; i++) {
      const next = [0];
      for (let j = 0; j < poly.length; j++) next.push(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, eccLen) {
    const gen = rsGenPoly(eccLen);
    const buf = data.slice();
    for (let i = 0; i < eccLen; i++) buf.push(0);
    for (let i = 0; i < data.length; i++) {
      const factor = buf[i];
      if (factor !== 0) {
        for (let j = 0; j < gen.length; j++) {
          buf[i + j] ^= gfMul(gen[j], factor);
        }
      }
    }
    return buf.slice(data.length);
  }

  // Tables capacité (version, ECC level M) — data codewords / blocks / ecc per block
  // [version-1]: { totalCw, dataCw, eccPerBlock, blocks: [[count, dataCw], ...] }
  const VERSIONS_M = {
    1:  { dataCw: 16,  eccPerBlock: 10, blocks: [[1, 16]] },
    2:  { dataCw: 28,  eccPerBlock: 16, blocks: [[1, 28]] },
    3:  { dataCw: 44,  eccPerBlock: 26, blocks: [[1, 44]] },
    4:  { dataCw: 64,  eccPerBlock: 18, blocks: [[2, 32]] },
    5:  { dataCw: 86,  eccPerBlock: 24, blocks: [[2, 43]] },
    6:  { dataCw: 108, eccPerBlock: 16, blocks: [[4, 27]] },
    7:  { dataCw: 124, eccPerBlock: 18, blocks: [[4, 31]] },
    8:  { dataCw: 154, eccPerBlock: 22, blocks: [[2, 38], [2, 39]] },
    9:  { dataCw: 182, eccPerBlock: 22, blocks: [[3, 36], [2, 37]] },
    10: { dataCw: 216, eccPerBlock: 26, blocks: [[4, 43], [1, 44]] },
  };

  // Capacité max en mode byte (ECC-M) pour chaque version, en chars
  // = (dataCw * 8 - mode(4) - charCount(16) - terminator) / 8
  function byteCapacity(v) {
    const info = VERSIONS_M[v];
    const charCountBits = v <= 9 ? 8 : 16;
    return Math.floor((info.dataCw * 8 - 4 - charCountBits) / 8);
  }

  function pickVersion(textLen) {
    for (let v = 1; v <= 10; v++) {
      if (byteCapacity(v) >= textLen) return v;
    }
    throw new Error("QR: text too long for version 10");
  }

  // Bit buffer
  function makeBitBuf() {
    const bits = [];
    return {
      put(val, n) {
        for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1);
      },
      length() { return bits.length; },
      toBytes() {
        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;
      },
    };
  }

  function buildDataBytes(text, version) {
    const info = VERSIONS_M[version];
    const charCountBits = version <= 9 ? 8 : 16;
    const buf = makeBitBuf();

    // Mode indicator: byte = 0100
    buf.put(0b0100, 4);
    buf.put(text.length, charCountBits);

    // Encode each byte (UTF-8)
    const bytes = [];
    for (let i = 0; i < text.length; i++) {
      const c = text.charCodeAt(i);
      if (c < 0x80) bytes.push(c);
      else if (c < 0x800) {
        bytes.push(0xC0 | (c >> 6));
        bytes.push(0x80 | (c & 0x3F));
      } else {
        bytes.push(0xE0 | (c >> 12));
        bytes.push(0x80 | ((c >> 6) & 0x3F));
        bytes.push(0x80 | (c & 0x3F));
      }
    }
    // (mais EMV est ASCII pur, on limite quand même à text.length)
    for (let i = 0; i < text.length; i++) {
      buf.put(text.charCodeAt(i), 8);
    }

    // Terminator (up to 4 zero bits, but no more than capacity)
    const totalDataBits = info.dataCw * 8;
    const term = Math.min(4, totalDataBits - buf.length());
    buf.put(0, term);

    // Pad to byte boundary
    while (buf.length() % 8 !== 0) buf.put(0, 1);

    // Get bytes, then pad with 0xEC, 0x11 alternating
    const dataBytes = buf.toBytes();
    const padPattern = [0xEC, 0x11];
    let pi = 0;
    while (dataBytes.length < info.dataCw) {
      dataBytes.push(padPattern[pi % 2]);
      pi++;
    }
    return dataBytes;
  }

  // Découper en blocs et calculer ECC, puis interleave
  function buildFinalCodewords(dataBytes, version) {
    const info = VERSIONS_M[version];
    const dataBlocks = [];
    const eccBlocks = [];
    let offset = 0;
    for (const [count, blockDataLen] of info.blocks) {
      for (let i = 0; i < count; i++) {
        const block = dataBytes.slice(offset, offset + blockDataLen);
        offset += blockDataLen;
        dataBlocks.push(block);
        eccBlocks.push(rsEncode(block, info.eccPerBlock));
      }
    }

    // Interleave data
    const result = [];
    let maxData = 0;
    for (const b of dataBlocks) maxData = Math.max(maxData, b.length);
    for (let i = 0; i < maxData; i++) {
      for (const b of dataBlocks) if (i < b.length) result.push(b[i]);
    }
    // Interleave ecc
    for (let i = 0; i < info.eccPerBlock; i++) {
      for (const b of eccBlocks) result.push(b[i]);
    }
    return result;
  }

  /* ---------- Matrix construction ---------- */

  function size(v) { return 21 + 4 * (v - 1); }

  function makeEmptyMatrix(v) {
    const n = size(v);
    const m = [];
    const reserved = [];
    for (let i = 0; i < n; i++) {
      m.push(new Array(n).fill(null));
      reserved.push(new Array(n).fill(false));
    }
    return { m, reserved, n };
  }

  function placeFinder(mat, 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 >= mat.n || cc < 0 || cc >= mat.n) continue;
        let v = 0;
        if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) {
          if (dr === 0 || dr === 6 || dc === 0 || dc === 6) v = 1;
          else if (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4) v = 1;
          else v = 0;
        } else {
          v = 0;
        }
        mat.m[rr][cc] = v;
        mat.reserved[rr][cc] = true;
      }
    }
  }

  function placeTimings(mat) {
    for (let i = 8; i < mat.n - 8; i++) {
      const v = (i % 2 === 0) ? 1 : 0;
      if (mat.m[6][i] === null) { mat.m[6][i] = v; mat.reserved[6][i] = true; }
      if (mat.m[i][6] === null) { mat.m[i][6] = v; mat.reserved[i][6] = true; }
    }
  }

  // Tables alignement (versions 2-10)
  const ALIGN = {
    2:  [6, 18],
    3:  [6, 22],
    4:  [6, 26],
    5:  [6, 30],
    6:  [6, 34],
    7:  [6, 22, 38],
    8:  [6, 24, 42],
    9:  [6, 26, 46],
    10: [6, 28, 50],
  };

  function placeAlignment(mat, version) {
    const positions = ALIGN[version];
    if (!positions) return;
    for (const r of positions) {
      for (const c of positions) {
        // skip si recouvre un finder
        if ((r <= 8 && c <= 8) || (r <= 8 && c >= mat.n - 9) || (r >= mat.n - 9 && c <= 8)) continue;
        for (let dr = -2; dr <= 2; dr++) {
          for (let dc = -2; dc <= 2; dc++) {
            const v = (Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0)) ? 1 : 0;
            mat.m[r + dr][c + dc] = v;
            mat.reserved[r + dr][c + dc] = true;
          }
        }
      }
    }
  }

  function reserveFormatAreas(mat) {
    // Format info: 9 bits autour finders
    for (let i = 0; i < 9; i++) {
      if (mat.m[8][i] === null) { mat.reserved[8][i] = true; }
      if (mat.m[i][8] === null) { mat.reserved[i][8] = true; }
    }
    for (let i = mat.n - 8; i < mat.n; i++) {
      mat.reserved[8][i] = true;
      mat.reserved[i][8] = true;
    }
    // Dark module (toujours noir)
    mat.m[mat.n - 8][8] = 1;
    mat.reserved[mat.n - 8][8] = true;
  }

  function placeData(mat, codewords) {
    const bits = [];
    for (const cw of codewords) {
      for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1);
    }
    let bitIdx = 0;
    let upward = true;
    let col = mat.n - 1;
    while (col > 0) {
      if (col === 6) col--; // skip timing column
      for (let i = 0; i < mat.n; i++) {
        const row = upward ? mat.n - 1 - i : i;
        for (let j = 0; j < 2; j++) {
          const c = col - j;
          if (!mat.reserved[row][c]) {
            mat.m[row][c] = (bitIdx < bits.length) ? bits[bitIdx] : 0;
            bitIdx++;
          }
        }
      }
      upward = !upward;
      col -= 2;
    }
  }

  // 8 patterns de masque
  const MASKS = [
    (r, c) => (r + c) % 2 === 0,
    (r, c) => r % 2 === 0,
    (r, c) => c % 3 === 0,
    (r, c) => (r + c) % 3 === 0,
    (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
    (r, c) => (r * c) % 2 + (r * c) % 3 === 0,
    (r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0,
    (r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0,
  ];

  function applyMask(matrix, reserved, n, maskFn) {
    const out = matrix.map(row => row.slice());
    for (let r = 0; r < n; r++) {
      for (let c = 0; c < n; c++) {
        if (!reserved[r][c] && maskFn(r, c)) {
          out[r][c] = out[r][c] ^ 1;
        }
      }
    }
    return out;
  }

  // Format info — ECC-M (01) + maskNum (3 bits) + 10 bits ECC + XOR mask
  function formatBits(maskNum) {
    const data = (0b01 << 3) | maskNum; // 5 bits
    let rem = data << 10;
    const gen = 0b10100110111;
    for (let i = 14; i >= 10; i--) {
      if (rem & (1 << i)) rem ^= gen << (i - 10);
    }
    const bits = ((data << 10) | rem) ^ 0b101010000010010;
    return bits; // 15 bits
  }

  function placeFormat(matrix, n, maskNum) {
    const bits = formatBits(maskNum);
    // Premier set (autour finder TL)
    for (let i = 0; i < 6; i++) matrix[i][8] = (bits >> i) & 1;
    matrix[7][8] = (bits >> 6) & 1;
    matrix[8][8] = (bits >> 7) & 1;
    matrix[8][7] = (bits >> 8) & 1;
    for (let i = 0; i < 6; i++) matrix[8][5 - i] = (bits >> (9 + i)) & 1;
    // Second set (autour finders TR + BL)
    for (let i = 0; i < 8; i++) matrix[n - 1 - i][8] = (bits >> i) & 1;
    for (let i = 0; i < 7; i++) matrix[8][n - 7 + i] = (bits >> (8 + i)) & 1;
  }

  // Pénalités pour scoring (4 règles standard)
  function score(mat, n) {
    let p = 0;
    // Règle 1: runs de 5+ même couleur
    for (let i = 0; i < n; i++) {
      let runR = 1, runC = 1;
      for (let j = 1; j < n; j++) {
        if (mat[i][j] === mat[i][j - 1]) { runR++; if (runR === 5) p += 3; else if (runR > 5) p++; }
        else runR = 1;
        if (mat[j][i] === mat[j - 1][i]) { runC++; if (runC === 5) p += 3; else if (runC > 5) p++; }
        else runC = 1;
      }
    }
    // Règle 2: blocs 2x2 même couleur
    for (let r = 0; r < n - 1; r++) {
      for (let c = 0; c < n - 1; c++) {
        if (mat[r][c] === mat[r][c+1] && mat[r][c] === mat[r+1][c] && mat[r][c] === mat[r+1][c+1]) p += 3;
      }
    }
    return p;
  }

  function generateQRMatrix(text) {
    const v = pickVersion(text.length);
    const dataBytes = buildDataBytes(text, v);
    const codewords = buildFinalCodewords(dataBytes, v);

    const n = size(v);
    const mat = makeEmptyMatrix(v);
    placeFinder(mat, 0, 0);
    placeFinder(mat, 0, n - 7);
    placeFinder(mat, n - 7, 0);
    placeAlignment(mat, v);
    placeTimings(mat);
    reserveFormatAreas(mat);
    placeData(mat, codewords);

    // Choisir le meilleur masque
    let best = null, bestScore = Infinity;
    for (let mk = 0; mk < 8; mk++) {
      const masked = applyMask(mat.m, mat.reserved, n, MASKS[mk]);
      placeFormat(masked, n, mk);
      const s = score(masked, n);
      if (s < bestScore) { bestScore = s; best = masked; }
    }
    return best;
  }

  /* ---------- Config + exposition globale ---------- */

  window.PIX_CONFIG = {
    key:  "71b7d8f7-0272-4778-a6c4-33dcebdb4f2a",
    name: "HOTEL OMEGA",
    city: "OIAPOQUE",
  };

  window.buildPixPayload = buildPixPayload;
  window.generateQRMatrix = generateQRMatrix;
})();
