///|
/// Encode a direct RGBA framebuffer (four 0..255 channels per pixel,
/// row-major) into a base64 PNG. Unlike the terminal framebuffer encoder
/// (which takes palette indices), this consumes the gfx software backend's
/// true-color output, so HTML can be rendered straight to a PNG for image
/// VRT tooling.
#borrow(rgba)
extern "js" fn rgba_to_png_base64_js(
  width : Int,
  height : Int,
  rgba : Array[Int],
) -> String =
  #|(width, height, rgba) => {
  #|  const zlib = typeof require === "function" ? require("node:zlib") : null;
  #|  let crcTable = globalThis.__craterPngCrcTable;
  #|  if (!crcTable) {
  #|    crcTable = new Uint32Array(256);
  #|    for (let n = 0; n < 256; n++) {
  #|      let c = n;
  #|      for (let k = 0; k < 8; k++) {
  #|        c = (c & 1) !== 0 ? (0xedb88320 ^ (c >>> 1)) >>> 0 : (c >>> 1) >>> 0;
  #|      }
  #|      crcTable[n] = c >>> 0;
  #|    }
  #|    globalThis.__craterPngCrcTable = crcTable;
  #|  }
  #|  function crc32(typeBuf, dataBuf) {
  #|    let crc = 0xffffffff;
  #|    for (let i = 0; i < typeBuf.length; i++) {
  #|      crc = (crcTable[(crc ^ typeBuf[i]) & 0xff] ^ (crc >>> 8)) >>> 0;
  #|    }
  #|    for (let i = 0; i < dataBuf.length; i++) {
  #|      crc = (crcTable[(crc ^ dataBuf[i]) & 0xff] ^ (crc >>> 8)) >>> 0;
  #|    }
  #|    return (crc ^ 0xffffffff) >>> 0;
  #|  }
  #|  function adler32(buf) {
  #|    let a = 1;
  #|    let b = 0;
  #|    for (let i = 0; i < buf.length; i++) {
  #|      a = (a + (buf[i] & 0xff)) % 65521;
  #|      b = (b + a) % 65521;
  #|    }
  #|    return ((b << 16) | a) >>> 0;
  #|  }
  #|  function deflateStore(raw) {
  #|    const parts = [Buffer.from([0x78, 0x01])];
  #|    for (let offset = 0; offset < raw.length;) {
  #|      const len = Math.min(65535, raw.length - offset);
  #|      const final = offset + len >= raw.length ? 1 : 0;
  #|      const block = Buffer.allocUnsafe(5 + len);
  #|      block[0] = final;
  #|      block.writeUInt16LE(len, 1);
  #|      block.writeUInt16LE((~len) & 0xffff, 3);
  #|      raw.copy(block, 5, offset, offset + len);
  #|      parts.push(block);
  #|      offset += len;
  #|    }
  #|    const checksum = Buffer.allocUnsafe(4);
  #|    checksum.writeUInt32BE(adler32(raw), 0);
  #|    parts.push(checksum);
  #|    return Buffer.concat(parts);
  #|  }
  #|  function deflatePngData(raw) {
  #|    if (zlib && typeof zlib.deflateSync === "function") {
  #|      return zlib.deflateSync(raw, { level: 1 });
  #|    }
  #|    return deflateStore(raw);
  #|  }
  #|  function chunk(type, data) {
  #|    const typeBuf = Buffer.from(type, "ascii");
  #|    const out = Buffer.allocUnsafe(8 + data.length + 4);
  #|    out.writeUInt32BE(data.length, 0);
  #|    typeBuf.copy(out, 4);
  #|    data.copy(out, 8);
  #|    out.writeUInt32BE(crc32(typeBuf, data), 8 + data.length);
  #|    return out;
  #|  }
  #|  const raw = Buffer.allocUnsafe(height * (width * 4 + 1));
  #|  let src = 0;
  #|  let dst = 0;
  #|  for (let y = 0; y < height; y++) {
  #|    raw[dst++] = 0;
  #|    for (let x = 0; x < width; x++) {
  #|      raw[dst++] = rgba[src++] & 0xff;
  #|      raw[dst++] = rgba[src++] & 0xff;
  #|      raw[dst++] = rgba[src++] & 0xff;
  #|      raw[dst++] = rgba[src++] & 0xff;
  #|    }
  #|  }
  #|  const compressed = deflatePngData(raw);
  #|  const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
  #|  const ihdr = Buffer.allocUnsafe(13);
  #|  ihdr.writeUInt32BE(width >>> 0, 0);
  #|  ihdr.writeUInt32BE(height >>> 0, 4);
  #|  ihdr[8] = 8;
  #|  ihdr[9] = 6;
  #|  ihdr[10] = 0;
  #|  ihdr[11] = 0;
  #|  ihdr[12] = 0;
  #|  const chunks = [
  #|    signature,
  #|    chunk("IHDR", ihdr),
  #|    chunk("IDAT", compressed),
  #|    chunk("IEND", Buffer.alloc(0)),
  #|  ];
  #|  const png = Buffer.concat(chunks);
  #|  return png.toString("base64");
  #|}

///|
/// Render an HTML document to a base64-encoded PNG via the gfx software
/// backend (image VRT). Companion to `renderHtmlToPaintTree` (geometry);
/// this returns true-color pixels.
pub fn renderHtmlToImagePngBase64(
  html : String,
  width : Int,
  height : Int,
) -> String {
  let image = @gfx_vrt.render_html_to_image(
    html,
    @types.Size::new(width.to_double(), height.to_double()),
  )
  rgba_to_png_base64_js(image.width, image.height, image.pixels)
}

///|
/// Render an HTML document to a flat RGBA pixel buffer (row-major, four
/// 0..255 channels per pixel) via the gfx software backend.
pub fn renderHtmlToImageRgba(
  html : String,
  width : Int,
  height : Int,
) -> Array[Int] {
  @gfx_vrt.render_html_to_image(
    html,
    @types.Size::new(width.to_double(), height.to_double()),
  ).pixels
}

///|
/// Render text to a base64-encoded PNG using the built-in 5x7 bitmap font
/// (black on white) via the gfx software backend. `scale` is the pixel size
/// of each font cell.
pub fn renderTextToImagePngBase64(
  text : String,
  width : Int,
  height : Int,
  scale : Int,
) -> String {
  let image = @gfx_vrt.render_text_to_image(text, width, height, scale~)
  rgba_to_png_base64_js(image.width, image.height, image.pixels)
}

///|
/// Install a TrueType/OpenType font (raw bytes as 0..255 ints) as the
/// global glyph provider, so HTML text renders real glyph shapes through
/// the gfx pipeline. Returns false if the font could not be parsed.
pub fn setFontProviderFromBytes(data : Array[Int]) -> Bool {
  let bytes = Bytes::from_array(data.map(fn(i) { (i & 0xff).to_byte() }))
  match @glyph.glyph_provider_from_bytes(bytes) {
    Some(provider) => {
      @glyph.set_glyph_provider(provider)
      true
    }
    None => false
  }
}

///|
/// Render HTML to a flat RGBA pixel buffer by driving crater's assembler
/// onto the gfx **web** backend (WebGPU/WebGL stub driver + the JS runtime
/// at globalThis.__craterGfxWeb). This is the same path a real WebGPU
/// device uses; the JS runtime rasterizes (CPU today, GPU when available).
pub fn renderHtmlViaWebBackendRgba(
  html : String,
  width : Int,
  height : Int,
) -> Array[Int] {
  @gfx_web.install_js_web_backend()
  let driver = @gfx_web.webgpu_driver(width, height)
  let root = @renderer_vrt.render_html_to_paint_tree(
    html,
    @types.Size::new(width.to_double(), height.to_double()),
  )
  @gfx_bridge.render_paint_tree(driver, root, width~, height~) catch {
    _ => ()
  }
  @gfx_web.web_backend_read_pixels()
}

///|
/// Render HTML to a base64 PNG via the gfx web backend (see
/// `renderHtmlViaWebBackendRgba`).
pub fn renderHtmlViaWebBackendPngBase64(
  html : String,
  width : Int,
  height : Int,
) -> String {
  let pixels = renderHtmlViaWebBackendRgba(html, width, height)
  rgba_to_png_base64_js(width, height, pixels)
}