/**
 * Render ANSI-escaped terminal text directly to a PNG image.
 *
 * Replaces the previous ansiToSvg → @resvg/resvg-wasm pipeline. The SVG was
 * just a lossy intermediate format for what is fundamentally a grid of
 * (char, fg-color, bold) cells on a flat background. This module skips SVG
 * entirely: it blits a bundled 24×48 bitmap font directly into an RGBA
 * Uint8Array, then encodes the result as a PNG using node:zlib.
 *
 * Why not resvg-wasm: 2.36MB of embedded WASM, a 2.1MB runtime font load
 * from a hardcoded system path (returning [] → blank screenshots when the
 * font isn't found), and ~224ms per render. This path is ~5–15ms, zero
 * external deps, identical output on mac/linux/windows.
 *
 * Font: Fira Code Regular rasterized at 24×48 with 8-bit anti-aliased alpha
 * (SIL OFL 1.1 — see scripts/LICENSE-FiraCode). Covers printable ASCII plus
 * the unicode chars used by /stats output. Regenerate with:
 *   bun scripts/generate-bitmap-font.ts
 */

import { deflateSync } from 'zlib'
import { stringWidth } from '../ink/stringWidth.js'
import {
  type AnsiColor,
  DEFAULT_BG,
  type ParsedLine,
  parseAnsi,
} from './ansiToSvg.js'

// Glyph cell size — rasterized at output resolution so the default scale=1
// is crisp (no nearest-neighbor upscaling artifacts).
const GLYPH_W = 24
const GLYPH_H = 48
const GLYPH_BYTES = GLYPH_W * GLYPH_H

// Packed font rasterized from Fira Code Regular (SIL OFL 1.1).
// Copyright (c) 2014-2021 The Fira Code Project Authors.
// License: scripts/LICENSE-FiraCode
// Format: [count:u16le][codepoint:u32le, alpha:GLYPH_W*GLYPH_H bytes]...
const FONT_B64 =
  '[encoded data, 205000 characters]'

// Dotted-box fallback for codepoints outside the bundled set.
const FALLBACK_GLYPH = makeFallbackGlyph()

function makeFallbackGlyph(): Uint8Array {
  const g = new Uint8Array(GLYPH_BYTES)
  for (let y = 2; y < GLYPH_H - 4; y++) {
    for (let x = 1; x < GLYPH_W - 1; x++) {
      const onBorder =
        y === 2 || y === GLYPH_H - 5 || x === 1 || x === GLYPH_W - 2
      if (onBorder && (x + y) % 2 === 0) g[y * GLYPH_W + x] = 255
    }
  }
  return g
}

const FONT: Map<number, Uint8Array> = decodeFont()

function decodeFont(): Map<number, Uint8Array> {
  const buf = Buffer.from(FONT_B64, 'base64')
  const count = buf.readUInt16LE(0)
  const map = new Map<number, Uint8Array>()
  let off = 2
  for (let i = 0; i < count; i++) {
    const cp = buf.readUInt32LE(off)
    off += 4
    map.set(cp, buf.subarray(off, off + GLYPH_BYTES))
    off += GLYPH_BYTES
  }
  return map
}

export type AnsiToPngOptions = {
  /** Integer zoom factor (nearest-neighbor). Default 1 — the font is already rasterized at output resolution. */
  scale?: number
  /** Horizontal padding in 1× pixels. Default 48. */
  paddingX?: number
  /** Vertical padding in 1× pixels. Default 48. */
  paddingY?: number
  /** Corner radius in 1× pixels. Default 16. */
  borderRadius?: number
  /** Background color. Default: dark gray (same as ansiToSvg). */
  background?: AnsiColor
}

/**
 * Render ANSI-escaped text directly to a PNG buffer.
 * Returns a Buffer containing a valid PNG (RGBA, 8-bit).
 */
export function ansiToPng(
  ansiText: string,
  options: AnsiToPngOptions = {},
): Buffer {
  const {
    scale = 1,
    paddingX = 48,
    paddingY = 48,
    borderRadius = 16,
    background = DEFAULT_BG,
  } = options

  const lines = parseAnsi(ansiText)
  // Trim trailing blank lines (same behavior as ansiToSvg).
  while (
    lines.length > 0 &&
    lines[lines.length - 1]!.every(span => span.text.trim() === '')
  ) {
    lines.pop()
  }
  if (lines.length === 0) {
    lines.push([{ text: '', color: background, bold: false }])
  }

  const cols = Math.max(1, ...lines.map(lineWidthCells))
  const rows = lines.length

  const width = (cols * GLYPH_W + paddingX * 2) * scale
  const height = (rows * GLYPH_H + paddingY * 2) * scale

  // RGBA buffer, pre-filled with the background color.
  const px = new Uint8Array(width * height * 4)
  fillBackground(px, background)
  if (borderRadius > 0) {
    roundCorners(px, width, height, borderRadius * scale)
  }

  // Blit glyphs.
  const padX = paddingX * scale
  const padY = paddingY * scale
  for (let row = 0; row < rows; row++) {
    let col = 0
    for (const span of lines[row]!) {
      for (const ch of span.text) {
        const cp = ch.codePointAt(0)!
        const cellW = stringWidth(ch)
        if (cellW === 0) continue // zero-width (combining marks, etc.)
        const x = padX + col * GLYPH_W * scale
        const y = padY + row * GLYPH_H * scale
        const shade = SHADE_ALPHA[cp]
        if (shade !== undefined) {
          blitShade(px, width, x, y, span.color, background, shade, scale)
        } else {
          const glyph = FONT.get(cp) ?? FALLBACK_GLYPH
          blitGlyph(px, width, x, y, glyph, span.color, span.bold, scale)
        }
        col += cellW
      }
    }
  }

  return encodePng(px, width, height)
}

/** Terminal column width of a parsed line. */
function lineWidthCells(line: ParsedLine): number {
  let w = 0
  for (const span of line) w += stringWidth(span.text)
  return w
}

function fillBackground(px: Uint8Array, bg: AnsiColor): void {
  for (let i = 0; i < px.length; i += 4) {
    px[i] = bg.r
    px[i + 1] = bg.g
    px[i + 2] = bg.b
    px[i + 3] = 255
  }
}

// Modern terminals render shade chars (░▒▓█) as solid blocks with opacity,
// not the classic VGA dither pattern. Alpha-blend toward background for the
// same look.
const SHADE_ALPHA: Record<number, number> = {
  0x2591: 0.25, // ░
  0x2592: 0.5, // ▒
  0x2593: 0.75, // ▓
  0x2588: 1.0, // █
}

function blitShade(
  px: Uint8Array,
  width: number,
  x: number,
  y: number,
  fg: AnsiColor,
  bg: AnsiColor,
  alpha: number,
  scale: number,
): void {
  const r = Math.round(fg.r * alpha + bg.r * (1 - alpha))
  const g = Math.round(fg.g * alpha + bg.g * (1 - alpha))
  const b = Math.round(fg.b * alpha + bg.b * (1 - alpha))
  const cellW = GLYPH_W * scale
  const cellH = GLYPH_H * scale
  for (let dy = 0; dy < cellH; dy++) {
    const rowBase = ((y + dy) * width + x) * 4
    for (let dx = 0; dx < cellW; dx++) {
      const i = rowBase + dx * 4
      px[i] = r
      px[i + 1] = g
      px[i + 2] = b
    }
  }
}

/**
 * Blit one glyph into the RGBA buffer at (x,y), scaled by `scale`
 * (nearest-neighbor). Alpha-composites over the existing background. Bold is
 * synthesized by boosting alpha toward opaque — a cheap approximation that
 * reads as heavier weight without needing a second font.
 */
function blitGlyph(
  px: Uint8Array,
  width: number,
  x: number,
  y: number,
  glyph: Uint8Array,
  color: AnsiColor,
  bold: boolean,
  scale: number,
): void {
  for (let gy = 0; gy < GLYPH_H; gy++) {
    for (let gx = 0; gx < GLYPH_W; gx++) {
      let a = glyph[gy * GLYPH_W + gx]!
      if (a === 0) continue
      if (bold) a = Math.min(255, a * 1.4)
      const inv = 255 - a
      for (let sy = 0; sy < scale; sy++) {
        const rowBase = ((y + gy * scale + sy) * width + x + gx * scale) * 4
        for (let sx = 0; sx < scale; sx++) {
          const i = rowBase + sx * 4
          px[i] = (color.r * a + px[i]! * inv) >> 8
          px[i + 1] = (color.g * a + px[i + 1]! * inv) >> 8
          px[i + 2] = (color.b * a + px[i + 2]! * inv) >> 8
        }
      }
    }
  }
}

/**
 * Zero out the alpha channel in the four corner regions outside a
 * quarter-circle of radius `r`. Produces rounded-rect corners.
 */
function roundCorners(
  px: Uint8Array,
  width: number,
  height: number,
  r: number,
): void {
  const r2 = r * r
  for (let dy = 0; dy < r; dy++) {
    for (let dx = 0; dx < r; dx++) {
      const ox = r - dx - 0.5
      const oy = r - dy - 0.5
      if (ox * ox + oy * oy <= r2) continue
      // Top-left, top-right, bottom-left, bottom-right.
      px[(dy * width + dx) * 4 + 3] = 0
      px[(dy * width + (width - 1 - dx)) * 4 + 3] = 0
      px[((height - 1 - dy) * width + dx) * 4 + 3] = 0
      px[((height - 1 - dy) * width + (width - 1 - dx)) * 4 + 3] = 0
    }
  }
}

// --- PNG encoding -----------------------------------------------------------

const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
const CRC_TABLE = makeCrcTable()

function makeCrcTable(): Uint32Array {
  const t = new Uint32Array(256)
  for (let n = 0; n < 256; n++) {
    let c = n
    for (let k = 0; k < 8; k++) {
      c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
    }
    t[n] = c >>> 0
  }
  return t
}

function crc32(data: Uint8Array): number {
  let c = 0xffffffff
  for (let i = 0; i < data.length; i++) {
    c = CRC_TABLE[(c ^ data[i]!) & 0xff]! ^ (c >>> 8)
  }
  return (c ^ 0xffffffff) >>> 0
}

function chunk(type: string, data: Uint8Array): Buffer {
  const body = Buffer.alloc(4 + data.length)
  body.write(type, 0, 'ascii')
  body.set(data, 4)
  const out = Buffer.alloc(12 + data.length)
  out.writeUInt32BE(data.length, 0)
  body.copy(out, 4)
  out.writeUInt32BE(crc32(body), 8 + data.length)
  return out
}

/**
 * Encode an RGBA pixel buffer as PNG. Minimal encoder: 8-bit depth,
 * color type 6 (RGBA), filter 0 (none) on every scanline, single IDAT.
 */
function encodePng(px: Uint8Array, width: number, height: number): Buffer {
  // IHDR
  const ihdr = Buffer.alloc(13)
  ihdr.writeUInt32BE(width, 0)
  ihdr.writeUInt32BE(height, 4)
  ihdr[8] = 8 // bit depth
  ihdr[9] = 6 // color type: RGBA
  ihdr[10] = 0 // compression: deflate
  ihdr[11] = 0 // filter method
  ihdr[12] = 0 // interlace: none

  // IDAT: each scanline prefixed with filter byte 0.
  const stride = width * 4
  const raw = Buffer.alloc(height * (stride + 1))
  for (let y = 0; y < height; y++) {
    const dst = y * (stride + 1)
    raw[dst] = 0
    raw.set(px.subarray(y * stride, (y + 1) * stride), dst + 1)
  }
  const idat = deflateSync(raw)

  return Buffer.concat([
    PNG_SIG,
    chunk('IHDR', ihdr),
    chunk('IDAT', idat),
    chunk('IEND', new Uint8Array(0)),
  ])
}

