import { inflateSync } from "node:zlib";
import type { Bitmap } from "../../src/lib/ocrPreprocess.js";

interface PngChunk {
  type: string;
  data: Buffer;
}

function readChunks(buffer: Buffer): PngChunk[] {
  const signature = buffer.subarray(0, 8);
  if (!signature.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
    throw new Error("Invalid PNG signature.");
  }
  const chunks: PngChunk[] = [];
  let offset = 8;
  while (offset + 12 <= buffer.length) {
    const length = buffer.readUInt32BE(offset);
    const type = buffer.toString("ascii", offset + 4, offset + 8);
    const dataStart = offset + 8;
    const dataEnd = dataStart + length;
    if (dataEnd + 4 > buffer.length) throw new Error("Invalid PNG chunk length.");
    chunks.push({ type, data: buffer.subarray(dataStart, dataEnd) });
    offset = dataEnd + 4;
    if (type === "IEND") break;
  }
  return chunks;
}

function paethPredictor(left: number, up: number, upperLeft: number) {
  const estimate = left + up - upperLeft;
  const leftDistance = Math.abs(estimate - left);
  const upDistance = Math.abs(estimate - up);
  const upperLeftDistance = Math.abs(estimate - upperLeft);
  if (leftDistance <= upDistance && leftDistance <= upperLeftDistance) return left;
  if (upDistance <= upperLeftDistance) return up;
  return upperLeft;
}

function unfilterScanlines(raw: Buffer, width: number, height: number, bytesPerPixel: number) {
  const stride = width * bytesPerPixel;
  const output = Buffer.alloc(stride * height);
  let rawOffset = 0;
  for (let row = 0; row < height; row++) {
    const filter = raw[rawOffset++];
    const rowOffset = row * stride;
    const previousRowOffset = rowOffset - stride;
    for (let col = 0; col < stride; col++) {
      const value = raw[rawOffset++];
      const left = col >= bytesPerPixel ? output[rowOffset + col - bytesPerPixel] : 0;
      const up = row > 0 ? output[previousRowOffset + col] : 0;
      const upperLeft = row > 0 && col >= bytesPerPixel ? output[previousRowOffset + col - bytesPerPixel] : 0;
      let restored = value;
      if (filter === 1) restored = value + left;
      else if (filter === 2) restored = value + up;
      else if (filter === 3) restored = value + Math.floor((left + up) / 2);
      else if (filter === 4) restored = value + paethPredictor(left, up, upperLeft);
      else if (filter !== 0) throw new Error(`Unsupported PNG filter: ${filter}`);
      output[rowOffset + col] = restored & 0xff;
    }
  }
  return output;
}

export function pngBufferToBitmap(buffer: Buffer): Bitmap {
  const chunks = readChunks(buffer);
  const ihdr = chunks.find((chunk) => chunk.type === "IHDR")?.data;
  if (!ihdr) throw new Error("PNG missing IHDR.");
  const width = ihdr.readUInt32BE(0);
  const height = ihdr.readUInt32BE(4);
  const bitDepth = ihdr[8];
  const colorType = ihdr[9];
  const compression = ihdr[10];
  const filter = ihdr[11];
  const interlace = ihdr[12];
  if (bitDepth !== 8 || compression !== 0 || filter !== 0 || interlace !== 0) {
    throw new Error("Unsupported PNG format.");
  }
  const sourceBytesPerPixel = colorType === 6 ? 4 : colorType === 2 ? 3 : 0;
  if (!sourceBytesPerPixel) throw new Error(`Unsupported PNG color type: ${colorType}`);
  const idat = Buffer.concat(chunks.filter((chunk) => chunk.type === "IDAT").map((chunk) => chunk.data));
  const unfiltered = unfilterScanlines(inflateSync(idat), width, height, sourceBytesPerPixel);
  if (colorType === 6) return { data: unfiltered, width, height };

  const rgba = Buffer.alloc(width * height * 4);
  for (let pixel = 0; pixel < width * height; pixel++) {
    rgba[pixel * 4] = unfiltered[pixel * 3];
    rgba[pixel * 4 + 1] = unfiltered[pixel * 3 + 1];
    rgba[pixel * 4 + 2] = unfiltered[pixel * 3 + 2];
    rgba[pixel * 4 + 3] = 255;
  }
  return { data: rgba, width, height };
}
