import React from "react";
import { View, StyleSheet } from "react-native";
import Svg, { Rect } from "react-native-svg";

// A from-scratch QR Code (ISO/IEC 18004) encoder - byte mode, error
// correction level L, versions 1-5. Earlier versions of this file drew
// finder-pattern-shaped noise that looked like a QR code but couldn't
// actually be decoded by anything. This one round-trips through a real
// Reed-Solomon + masking + zigzag-placement pipeline, the same structure
// any standard QR reader expects.

const GF_EXP = new Array(512);
const GF_LOG = new Array(256);
(function initGaloisField() {
  let x = 1;
  for (let i = 0; i < 255; i++) {
    GF_EXP[i] = x;
    GF_LOG[x] = i;
    x <<= 1;
    if (x & 0x100) x ^= 0x11d;
  }
  for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255];
})();

function gfMul(a: number, b: number): number {
  if (a === 0 || b === 0) return 0;
  return GF_EXP[GF_LOG[a] + GF_LOG[b]];
}

function rsGeneratorPoly(degree: number): number[] {
  let poly = [1];
  for (let i = 0; i < degree; i++) {
    const next = new Array(poly.length + 1).fill(0);
    for (let j = 0; j < poly.length; j++) {
      next[j] ^= gfMul(poly[j], 1);
      next[j + 1] ^= gfMul(poly[j], GF_EXP[i]);
    }
    poly = next;
  }
  return poly;
}

function rsComputeRemainder(dataCodewords: number[], ecCount: number): number[] {
  const gen = rsGeneratorPoly(ecCount);
  const res = dataCodewords.slice();
  for (let i = 0; i < ecCount; i++) res.push(0);
  for (let i = 0; i < dataCodewords.length; i++) {
    const coef = res[i];
    if (coef === 0) continue;
    for (let j = 0; j < gen.length; j++) {
      res[i + j] ^= gfMul(gen[j], coef);
    }
  }
  return res.slice(dataCodewords.length);
}

interface VersionInfo {
  size: number;
  dataCw: number;
  ecCw: number;
  align: number | null;
}

const VERSION_INFO: Record<number, VersionInfo> = {
  1: { size: 21, dataCw: 19, ecCw: 7, align: null },
  2: { size: 25, dataCw: 34, ecCw: 10, align: 18 },
  3: { size: 29, dataCw: 55, ecCw: 15, align: 22 },
  4: { size: 33, dataCw: 80, ecCw: 20, align: 26 },
  5: { size: 37, dataCw: 108, ecCw: 26, align: 30 },
};

function utf8Bytes(text: string): number[] {
  const bytes: number[] = [];
  for (let i = 0; i < text.length; i++) {
    let code = text.codePointAt(i)!;
    if (code > 0xffff) i++; // consumed a surrogate pair
    if (code < 0x80) {
      bytes.push(code);
    } else if (code < 0x800) {
      bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
    } else if (code < 0x10000) {
      bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
    } else {
      bytes.push(
        0xf0 | (code >> 18),
        0x80 | ((code >> 12) & 0x3f),
        0x80 | ((code >> 6) & 0x3f),
        0x80 | (code & 0x3f)
      );
    }
  }
  return bytes;
}

function chooseVersion(byteLen: number): number {
  for (const v of [1, 2, 3, 4, 5]) {
    const info = VERSION_INFO[v];
    const overheadBits = 4 + 8; // mode indicator + 8-bit char count (versions 1-9)
    if (byteLen * 8 + overheadBits + 4 <= info.dataCw * 8) return v;
  }
  // Falls back to the largest supported version rather than throwing - truncates instead of crashing the screen if something unexpectedly long
  // gets passed in.
  return 5;
}

function buildDataCodewords(bytes: number[], version: number): number[] {
  const info = VERSION_INFO[version];
  const bits: number[] = [];
  const pushBits = (value: number, len: number) => {
    for (let i = len - 1; i >= 0; i--) bits.push((value >> i) & 1);
  };

  const capacityBits = info.dataCw * 8;
  const maxBytes = Math.floor((capacityBits - 12) / 8);
  const trimmed = bytes.slice(0, Math.max(0, maxBytes));

  pushBits(0b0100, 4);
  pushBits(trimmed.length, 8);
  for (const b of trimmed) pushBits(b, 8);

  for (let i = 0; i < 4 && bits.length < capacityBits; i++) bits.push(0);
  while (bits.length % 8 !== 0) bits.push(0);

  const codewords: number[] = [];
  for (let i = 0; i < bits.length; i += 8) {
    let byte = 0;
    for (let j = 0; j < 8; j++) byte = (byte << 1) | bits[i + j];
    codewords.push(byte);
  }
  const padBytes = [0xec, 0x11];
  let p = 0;
  while (codewords.length < info.dataCw) {
    codewords.push(padBytes[p % 2]);
    p++;
  }
  return codewords;
}

function isFunctionModule(size: number, version: number, r: number, c: number): boolean {
  const inFinderBlock =
    (r < 8 && c < 8) || (r < 8 && c >= size - 8) || (r >= size - 8 && c < 8);
  if (inFinderBlock) return true;
  if (r === 6 || c === 6) return true;
  const a = VERSION_INFO[version].align;
  if (a !== null && Math.abs(r - a) <= 2 && Math.abs(c - a) <= 2) return true;
  return false;
}

function drawFinder(matrix: number[][], top: number, left: number): void {
  const size = matrix.length;
  for (let r = -1; r <= 7; r++) {
    for (let c = -1; c <= 7; c++) {
      const rr = top + r;
      const cc = left + c;
      if (rr < 0 || cc < 0 || rr >= size || cc >= size) continue;
      if (r === -1 || r === 7 || c === -1 || c === 7) {
        matrix[rr][cc] = 0;
      } else {
        const dark = r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4);
        matrix[rr][cc] = dark ? 1 : 0;
      }
    }
  }
}

function drawAlignment(matrix: number[][], center: number): void {
  for (let r = -2; r <= 2; r++) {
    for (let c = -2; c <= 2; c++) {
      const dark = r === -2 || r === 2 || c === -2 || c === 2 || (r === 0 && c === 0);
      matrix[center + r][center + c] = dark ? 1 : 0;
    }
  }
}

function computeFormatBits(ecLevelBits: number, maskPattern: number): number {
  const data5 = (ecLevelBits << 3) | maskPattern;
  let rem = data5 << 10;
  for (let i = 14; i >= 10; i--) {
    if ((rem >> i) & 1) rem ^= 0x537 << (i - 10);
  }
  const formatBits = (data5 << 10) | rem;
  return formatBits ^ 0x5412;
}

function placeFormatInfo(m: number[][], size: number, fmt: number): void {
  const bit = (i: number) => (fmt >> i) & 1;
  for (let i = 0; i <= 5; i++) m[8][i] = bit(i);
  m[8][7] = bit(6);
  m[8][8] = bit(7);
  m[7][8] = bit(8);
  for (let i = 9; i < 15; i++) m[14 - i][8] = bit(i);

  for (let i = 0; i <= 7; i++) m[size - 1 - i][8] = bit(i);
  for (let i = 8; i < 15; i++) m[8][size - 15 + i] = bit(i);
}

function maskFunction(pattern: number, r: number, c: number): boolean {
  switch (pattern) {
    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;
    default: return false;
  }
}

function penalty(m: number[][], size: number): number {
  let score = 0;
  for (let r = 0; r < size; r++) {
    let runColor = m[r][0];
    let runLen = 1;
    for (let c = 1; c < size; c++) {
      if (m[r][c] === runColor) {
        runLen++;
      } else {
        if (runLen >= 5) score += 3 + (runLen - 5);
        runColor = m[r][c];
        runLen = 1;
      }
    }
    if (runLen >= 5) score += 3 + (runLen - 5);
  }
  for (let c = 0; c < size; c++) {
    let runColor = m[0][c];
    let runLen = 1;
    for (let r = 1; r < size; r++) {
      if (m[r][c] === runColor) {
        runLen++;
      } else {
        if (runLen >= 5) score += 3 + (runLen - 5);
        runColor = m[r][c];
        runLen = 1;
      }
    }
    if (runLen >= 5) score += 3 + (runLen - 5);
  }
  for (let r = 0; r < size - 1; r++) {
    for (let c = 0; c < size - 1; c++) {
      const v = m[r][c];
      if (v === m[r][c + 1] && v === m[r + 1][c] && v === m[r + 1][c + 1]) score += 3;
    }
  }
  let dark = 0;
  for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) dark += m[r][c];
  const percent = (dark * 100) / (size * size);
  const prevMultiple = Math.floor(percent / 5) * 5;
  score += Math.min(Math.abs(prevMultiple - 50), Math.abs(prevMultiple + 5 - 50)) * 2;
  return score;
}

function buildMatrix(version: number, allCodewords: number[]): number[][] {
  const size = VERSION_INFO[version].size;
  const base: number[][] = Array.from({ length: size }, () => new Array(size).fill(-1));

  drawFinder(base, 0, 0);
  drawFinder(base, 0, size - 7);
  drawFinder(base, size - 7, 0);

  for (let i = 8; i < size - 8; i++) {
    base[6][i] = i % 2 === 0 ? 1 : 0;
    base[i][6] = i % 2 === 0 ? 1 : 0;
  }

  const align = VERSION_INFO[version].align;
  if (align !== null) drawAlignment(base, align);

  const bits: number[] = [];
  for (const cw of allCodewords) {
    for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1);
  }

  let bitIndex = 0;
  const dataModules: [number, number, number][] = [];
  for (let right = size - 1; right >= 1; right -= 2) {
    if (right === 6) right = 5;
    for (let vert = 0; vert < size; vert++) {
      const upward = ((right + 1) & 2) === 0;
      for (let j = 0; j < 2; j++) {
        const c = right - j;
        const r = upward ? size - 1 - vert : vert;
        if (isFunctionModule(size, version, r, c)) continue;
        if ((r === 8 && (c <= 8 || c >= size - 8)) || (c === 8 && (r <= 8 || r >= size - 8))) {
          continue;
        }
        const bit = bitIndex < bits.length ? bits[bitIndex] : 0;
        bitIndex++;
        dataModules.push([r, c, bit]);
      }
    }
  }

  let best: number[][] | null = null;
  let bestScore = Infinity;

  for (let p = 0; p < 8; p++) {
    const m = base.map((row) => row.slice());
    for (const [r, c, bit] of dataModules) {
      const flip = maskFunction(p, r, c);
      m[r][c] = flip ? bit ^ 1 : bit;
    }
    m[size - 8][8] = 1; // dark module
    placeFormatInfo(m, size, computeFormatBits(0b01, p));

    const s = penalty(m, size);
    if (s < bestScore) {
      bestScore = s;
      best = m;
    }
  }

  return best!;
}

function encodeQR(text: string): { matrix: number[][]; size: number } {
  const bytes = utf8Bytes(text);
  const version = chooseVersion(bytes.length);
  const dataCw = buildDataCodewords(bytes, version);
  const ecCw = rsComputeRemainder(dataCw, VERSION_INFO[version].ecCw);
  const matrix = buildMatrix(version, dataCw.concat(ecCw));
  return { matrix, size: VERSION_INFO[version].size };
}

interface QRCodeProps {
  value: string;
  size?: number;
  backgroundColor?: string;
  foregroundColor?: string;
}

export function QRCode({
  value,
  size = 200,
  backgroundColor = "#ffffff",
  foregroundColor = "#000000",
}: QRCodeProps) {
  const { matrix, size: moduleCount } = encodeQR(value);
  // a little breathing room around the modules so it scans reliably
  const quietZone = 2;
  const totalModules = moduleCount + quietZone * 2;
  const cellSize = size / totalModules;

  return (
    <View style={[styles.container, { width: size, height: size }]} pointerEvents="none">
      <Svg width={size} height={size} pointerEvents="none">
        <Rect width={size} height={size} fill={backgroundColor} />
        {matrix.map((row, y) =>
          row.map((cell, x) =>
            cell === 1 ? (
              <Rect
                key={`${x}-${y}`}
                x={(x + quietZone) * cellSize}
                y={(y + quietZone) * cellSize}
                width={cellSize}
                height={cellSize}
                fill={foregroundColor}
              />
            ) : null
          )
        )}
      </Svg>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    overflow: "hidden",
    borderRadius: 8,
  },
});
