type Cell = boolean | null

interface BlockGroup {
  count: number
  dataCodewords: number
}

interface VersionSpec {
  version: number
  size: number
  dataCodewords: number
  eccCodewords: number
  blockGroups: BlockGroup[]
  capacity: number
  alignment: number[]
}

export interface QrCodeMatrix {
  size: number
  modules: boolean[][]
}

const VERSION_SPECS: VersionSpec[] = [
  { version: 1, size: 21, dataCodewords: 16, eccCodewords: 10, blockGroups: [{ count: 1, dataCodewords: 16 }], capacity: 14, alignment: [] },
  { version: 2, size: 25, dataCodewords: 28, eccCodewords: 16, blockGroups: [{ count: 1, dataCodewords: 28 }], capacity: 26, alignment: [6, 18] },
  { version: 3, size: 29, dataCodewords: 44, eccCodewords: 26, blockGroups: [{ count: 1, dataCodewords: 44 }], capacity: 42, alignment: [6, 22] },
  { version: 4, size: 33, dataCodewords: 64, eccCodewords: 18, blockGroups: [{ count: 2, dataCodewords: 32 }], capacity: 62, alignment: [6, 26] },
  { version: 5, size: 37, dataCodewords: 86, eccCodewords: 24, blockGroups: [{ count: 2, dataCodewords: 43 }], capacity: 84, alignment: [6, 30] },
  { version: 6, size: 41, dataCodewords: 108, eccCodewords: 16, blockGroups: [{ count: 4, dataCodewords: 27 }], capacity: 106, alignment: [6, 34] },
  { version: 7, size: 45, dataCodewords: 124, eccCodewords: 18, blockGroups: [{ count: 4, dataCodewords: 31 }], capacity: 122, alignment: [6, 22, 38] },
  { version: 8, size: 49, dataCodewords: 154, eccCodewords: 22, blockGroups: [{ count: 2, dataCodewords: 38 }, { count: 2, dataCodewords: 39 }], capacity: 152, alignment: [6, 24, 42] },
  { version: 9, size: 53, dataCodewords: 182, eccCodewords: 22, blockGroups: [{ count: 3, dataCodewords: 36 }, { count: 2, dataCodewords: 37 }], capacity: 180, alignment: [6, 26, 46] },
  { version: 10, size: 57, dataCodewords: 216, eccCodewords: 26, blockGroups: [{ count: 4, dataCodewords: 43 }, { count: 1, dataCodewords: 44 }], capacity: 213, alignment: [6, 28, 50] },
]

const FORMAT_XOR = 0x54_12
const FORMAT_POLY = 0x5_37
const VERSION_POLY = 0x1f_25

const gfExp = Array.from({ length: 512 }, () => 0)
const gfLog = Array.from({ length: 256 }, () => 0)
let x = 1
for (let i = 0; i < 255; i += 1) {
  gfExp[i] = x
  gfLog[x] = i
  x <<= 1
  if (x & 0x1_00) {
    x ^= 0x1_1d
  }
}
for (let i = 255; i < 512; i += 1) {
  gfExp[i] = gfExp[i - 255]
}

export function createQrCodeMatrix (value: string): QrCodeMatrix {
  const data = [...new TextEncoder().encode(value)]
  const spec = VERSION_SPECS.find(item => data.length <= item.capacity)
  if (!spec) {
    throw new Error('QR Code 內容過長，請將 payload 控制在 213 bytes 以內')
  }

  const dataCodewords = createDataCodewords(data, spec)
  const codewords = createFinalCodewords(dataCodewords, spec)
  const dataBits = codewords.flatMap(codeword => byteToBits(codeword))
  const base = createBaseMatrix(spec)

  let bestModules = base.modules
  let bestScore = Number.POSITIVE_INFINITY

  for (let mask = 0; mask < 8; mask += 1) {
    const modules = cloneMatrix(base.modules)
    placeDataBits(modules, base.reserved, dataBits, mask)
    drawFormatBits(modules, spec.size, mask)

    if (spec.version >= 7) {
      drawVersionBits(modules, spec)
    }

    const score = getPenaltyScore(modules)
    if (score < bestScore) {
      bestScore = score
      bestModules = modules
    }
  }

  return {
    size: spec.size,
    modules: bestModules.map(row => row.map(Boolean)),
  }
}

export function createQrCodeSvg (value: string, options: { scale?: number, quietZone?: number } = {}) {
  const { size, modules } = createQrCodeMatrix(value)
  const quietZone = options.quietZone ?? 4
  const scale = options.scale ?? 6
  const viewSize = size + quietZone * 2
  const rects: string[] = []

  for (const [y, row] of modules.entries()) {
    for (const [x, dark] of row.entries()) {
      if (dark) {
        rects.push(`<rect x="${x + quietZone}" y="${y + quietZone}" width="1" height="1"/>`)
      }
    }
  }

  return [
    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${viewSize} ${viewSize}" width="${viewSize * scale}" height="${viewSize * scale}" shape-rendering="crispEdges">`,
    '<rect width="100%" height="100%" fill="#fff"/>',
    '<g fill="#000">',
    ...rects,
    '</g>',
    '</svg>',
  ].join('')
}

function createDataCodewords (data: number[], spec: VersionSpec) {
  const bits: number[] = []
  appendBits(bits, 0b0100, 4)
  appendBits(bits, data.length, spec.version < 10 ? 8 : 16)
  for (const byte of data) {
    appendBits(bits, byte, 8)
  }

  const capacityBits = spec.dataCodewords * 8
  appendBits(bits, 0, Math.min(4, capacityBits - bits.length))

  while (bits.length % 8 !== 0) {
    bits.push(0)
  }

  const codewords: number[] = []
  for (let i = 0; i < bits.length; i += 8) {
    codewords.push(bitsToByte(bits.slice(i, i + 8)))
  }

  for (let pad = 0xec; codewords.length < spec.dataCodewords; pad ^= 0xec ^ 0x11) {
    codewords.push(pad)
  }

  return codewords
}

function createFinalCodewords (dataCodewords: number[], spec: VersionSpec) {
  const blocks: { data: number[], ecc: number[] }[] = []
  let offset = 0

  for (const group of spec.blockGroups) {
    for (let i = 0; i < group.count; i += 1) {
      const data = dataCodewords.slice(offset, offset + group.dataCodewords)
      offset += group.dataCodewords
      blocks.push({ data, ecc: createErrorCorrectionCodewords(data, spec.eccCodewords) })
    }
  }

  const result: number[] = []
  const maxDataLength = Math.max(...blocks.map(block => block.data.length))

  for (let i = 0; i < maxDataLength; i += 1) {
    for (const block of blocks) {
      if (i < block.data.length) {
        result.push(block.data[i])
      }
    }
  }

  for (let i = 0; i < spec.eccCodewords; i += 1) {
    for (const block of blocks) {
      result.push(block.ecc[i])
    }
  }

  return result
}

function createErrorCorrectionCodewords (data: number[], degree: number) {
  const generator = createGeneratorPolynomial(degree)
  const result = Array.from({ length: degree }, () => 0)

  for (const byte of data) {
    const factor = byte ^ result.shift()!
    result.push(0)

    for (const [index, coefficient] of generator.entries()) {
      result[index] ^= gfMultiply(coefficient, factor)
    }
  }

  return result
}

function createGeneratorPolynomial (degree: number) {
  let result = [1]

  for (let i = 0; i < degree; i += 1) {
    const next = Array.from({ length: result.length + 1 }, () => 0)
    for (const [index, coefficient] of result.entries()) {
      next[index] ^= gfMultiply(coefficient, 1)
      next[index + 1] ^= gfMultiply(coefficient, gfExp[i])
    }
    result = next
  }

  return result.slice(1)
}

function createBaseMatrix (spec: VersionSpec) {
  const modules = Array.from({ length: spec.size }, (): Cell[] => {
    return Array.from({ length: spec.size }, (): Cell => null)
  })
  const reserved = Array.from({ length: spec.size }, (): boolean[] => {
    return Array.from({ length: spec.size }, () => false)
  })

  drawFinderPattern(modules, reserved, 3, 3)
  drawFinderPattern(modules, reserved, spec.size - 4, 3)
  drawFinderPattern(modules, reserved, 3, spec.size - 4)
  drawTimingPatterns(modules, reserved)
  drawAlignmentPatterns(modules, reserved, spec.alignment)
  drawDarkModule(modules, reserved, spec.size)
  reserveFormatAreas(reserved, spec.size)

  if (spec.version >= 7) {
    reserveVersionAreas(reserved, spec.size)
  }

  return { modules, reserved }
}

function drawFinderPattern (modules: Cell[][], reserved: boolean[][], centerX: number, centerY: number) {
  for (let y = -4; y <= 4; y += 1) {
    for (let x = -4; x <= 4; x += 1) {
      const xx = centerX + x
      const yy = centerY + y
      if (!isInBounds(modules.length, xx, yy)) {
        continue
      }

      const distance = Math.max(Math.abs(x), Math.abs(y))
      modules[yy][xx] = distance !== 2 && distance !== 4
      reserved[yy][xx] = true
    }
  }
}

function drawTimingPatterns (modules: Cell[][], reserved: boolean[][]) {
  for (let i = 8; i < modules.length - 8; i += 1) {
    const dark = i % 2 === 0
    modules[6][i] = dark
    modules[i][6] = dark
    reserved[6][i] = true
    reserved[i][6] = true
  }
}

function drawAlignmentPatterns (modules: Cell[][], reserved: boolean[][], positions: number[]) {
  for (const y of positions) {
    for (const x of positions) {
      if (reserved[y][x]) {
        continue
      }

      for (let dy = -2; dy <= 2; dy += 1) {
        for (let dx = -2; dx <= 2; dx += 1) {
          const distance = Math.max(Math.abs(dx), Math.abs(dy))
          modules[y + dy][x + dx] = distance !== 1
          reserved[y + dy][x + dx] = true
        }
      }
    }
  }
}

function drawDarkModule (modules: Cell[][], reserved: boolean[][], size: number) {
  modules[size - 8][8] = true
  reserved[size - 8][8] = true
}

function reserveFormatAreas (reserved: boolean[][], size: number) {
  for (let i = 0; i <= 5; i += 1) {
    reserved[8][i] = true
    reserved[i][8] = true
  }

  reserved[8][7] = true
  reserved[8][8] = true
  reserved[7][8] = true

  for (let i = 0; i < 8; i += 1) {
    reserved[8][size - 1 - i] = true
  }

  for (let i = 8; i < 15; i += 1) {
    reserved[size - 15 + i][8] = true
  }
}

function reserveVersionAreas (reserved: boolean[][], size: number) {
  for (let i = 0; i < 6; i += 1) {
    for (let j = 0; j < 3; j += 1) {
      reserved[size - 11 + j][i] = true
      reserved[i][size - 11 + j] = true
    }
  }
}

function placeDataBits (modules: Cell[][], reserved: boolean[][], bits: number[], mask: number) {
  const size = modules.length
  let bitIndex = 0
  let direction = -1
  let y = size - 1

  for (let right = size - 1; right >= 1; right -= 2) {
    if (right === 6) {
      right -= 1
    }

    while (y >= 0 && y < size) {
      for (let column = 0; column < 2; column += 1) {
        const x = right - column
        if (!reserved[y][x]) {
          const bit = bitIndex < bits.length ? bits[bitIndex] === 1 : false
          modules[y][x] = bit !== getMaskBit(mask, x, y)
          bitIndex += 1
        }
      }
      y += direction
    }

    y -= direction
    direction = -direction
  }
}

function drawFormatBits (modules: Cell[][], size: number, mask: number) {
  const bits = getFormatBits(mask)

  for (let i = 0; i <= 5; i += 1) {
    setModule(modules, 8, i, getBit(bits, i))
  }
  setModule(modules, 8, 7, getBit(bits, 6))
  setModule(modules, 8, 8, getBit(bits, 7))
  setModule(modules, 7, 8, getBit(bits, 8))
  for (let i = 9; i < 15; i += 1) {
    setModule(modules, 14 - i, 8, getBit(bits, i))
  }

  for (let i = 0; i < 8; i += 1) {
    setModule(modules, size - 1 - i, 8, getBit(bits, i))
  }
  for (let i = 8; i < 15; i += 1) {
    setModule(modules, 8, size - 15 + i, getBit(bits, i))
  }
}

function drawVersionBits (modules: Cell[][], spec: VersionSpec) {
  const bits = getVersionBits(spec.version)
  const size = spec.size

  for (let i = 0; i < 18; i += 1) {
    const bit = getBit(bits, i)
    const x = i % 3
    const y = Math.floor(i / 3)
    setModule(modules, size - 11 + x, y, bit)
    setModule(modules, y, size - 11 + x, bit)
  }
}

function getFormatBits (mask: number) {
  let data = mask
  let bits = data << 10

  for (let i = 4; i >= 0; i -= 1) {
    if (((bits >>> (i + 10)) & 1) === 1) {
      bits ^= FORMAT_POLY << i
    }
  }

  data = (data << 10) | (bits & 0x03_ff)
  return data ^ FORMAT_XOR
}

function getVersionBits (version: number) {
  let bits = version << 12

  for (let i = 5; i >= 0; i -= 1) {
    if (((bits >>> (i + 12)) & 1) === 1) {
      bits ^= VERSION_POLY << i
    }
  }

  return (version << 12) | (bits & 0x0f_ff)
}

function getPenaltyScore (modules: Cell[][]) {
  const size = modules.length
  let score = 0

  for (let y = 0; y < size; y += 1) {
    score += getRunPenalty(modules[y].map(Boolean))
  }

  for (let x = 0; x < size; x += 1) {
    score += getRunPenalty(modules.map(row => Boolean(row[x])))
  }

  for (let y = 0; y < size - 1; y += 1) {
    for (let x = 0; x < size - 1; x += 1) {
      const dark = modules[y][x]
      if (modules[y][x + 1] === dark && modules[y + 1][x] === dark && modules[y + 1][x + 1] === dark) {
        score += 3
      }
    }
  }

  score += getFinderLikePenalty(modules)

  const darkCount = modules.flat().filter(Boolean).length
  const percent = (darkCount * 100) / (size * size)
  score += Math.floor(Math.abs(percent - 50) / 5) * 10

  return score
}

function getRunPenalty (line: boolean[]) {
  let score = 0
  let runColor = line[0]
  let runLength = 1

  for (let i = 1; i < line.length; i += 1) {
    if (line[i] === runColor) {
      runLength += 1
    } else {
      if (runLength >= 5) {
        score += runLength - 2
      }
      runColor = line[i]
      runLength = 1
    }
  }

  return runLength >= 5 ? score + runLength - 2 : score
}

function getFinderLikePenalty (modules: Cell[][]) {
  const pattern = '10111010000'
  const reversePattern = '00001011101'
  const size = modules.length
  let score = 0

  const scan = (line: boolean[]) => {
    const text = line.map(bit => (bit ? '1' : '0')).join('')
    for (let i = 0; i <= text.length - pattern.length; i += 1) {
      const chunk = text.slice(i, i + pattern.length)
      if (chunk === pattern || chunk === reversePattern) {
        score += 40
      }
    }
  }

  for (const row of modules) {
    scan(row.map(Boolean))
  }
  for (let x = 0; x < size; x += 1) {
    scan(modules.map(row => Boolean(row[x])))
  }

  return score
}

function getMaskBit (mask: number, x: number, y: number) {
  switch (mask) {
    case 0: {
      return (x + y) % 2 === 0
    }
    case 1: {
      return y % 2 === 0
    }
    case 2: {
      return x % 3 === 0
    }
    case 3: {
      return (x + y) % 3 === 0
    }
    case 4: {
      return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0
    }
    case 5: {
      return ((x * y) % 2) + ((x * y) % 3) === 0
    }
    case 6: {
      return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0
    }
    case 7: {
      return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0
    }
    default: {
      return false
    }
  }
}

function appendBits (target: number[], value: number, length: number) {
  for (let i = length - 1; i >= 0; i -= 1) {
    target.push((value >>> i) & 1)
  }
}

function byteToBits (value: number) {
  const bits: number[] = []
  appendBits(bits, value, 8)
  return bits
}

function bitsToByte (bits: number[]) {
  return bits.reduce((value, bit) => (value << 1) | bit, 0)
}

function gfMultiply (a: number, b: number) {
  if (a === 0 || b === 0) {
    return 0
  }

  return gfExp[gfLog[a] + gfLog[b]]
}

function getBit (value: number, index: number) {
  return ((value >>> index) & 1) === 1
}

function cloneMatrix (modules: Cell[][]) {
  return modules.map(row => [...row])
}

function setModule (modules: Cell[][], x: number, y: number, value: boolean) {
  modules[y][x] = value
}

function isInBounds (size: number, x: number, y: number) {
  return x >= 0 && y >= 0 && x < size && y < size
}
