import { Buffer } from "node:buffer";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { deflateSync, inflateSync } from "node:zlib";

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const iconsDir = join(root, "src-tauri", "icons");
const sourcePath = join(iconsDir, "GitCat_org.png");

const pngSizes = [32, 64, 128, 512];
const icoSizes = [16, 24, 32, 48, 64, 128, 256];

function decodePng(buffer) {
  if (buffer.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") {
    throw new Error("Source icon must be a PNG file.");
  }

  let width = 0;
  let height = 0;
  const idatChunks = [];

  for (let offset = 8; offset < buffer.length; ) {
    const length = buffer.readUInt32BE(offset);
    const type = buffer.subarray(offset + 4, offset + 8).toString("ascii");
    const dataStart = offset + 8;
    const dataEnd = dataStart + length;
    const data = buffer.subarray(dataStart, dataEnd);

    if (type === "IHDR") {
      width = data.readUInt32BE(0);
      height = data.readUInt32BE(4);
      if (data[8] !== 8 || data[9] !== 6) {
        throw new Error("Source icon must be an 8-bit RGBA PNG.");
      }
    } else if (type === "IDAT") {
      idatChunks.push(data);
    } else if (type === "IEND") {
      break;
    }

    offset = dataEnd + 4;
  }

  const stride = width * 4;
  const raw = inflateSync(Buffer.concat(idatChunks));
  const rgba = Buffer.alloc(width * height * 4);

  for (let y = 0; y < height; y += 1) {
    const rowStart = y * (stride + 1);
    const filter = raw[rowStart];
    const input = raw.subarray(rowStart + 1, rowStart + 1 + stride);
    const priorOffset = (y - 1) * stride;
    const outputOffset = y * stride;

    for (let x = 0; x < stride; x += 1) {
      const left = x >= 4 ? rgba[outputOffset + x - 4] : 0;
      const up = y > 0 ? rgba[priorOffset + x] : 0;
      const upLeft = y > 0 && x >= 4 ? rgba[priorOffset + x - 4] : 0;
      let value = input[x];

      if (filter === 1) value += left;
      else if (filter === 2) value += up;
      else if (filter === 3) value += Math.floor((left + up) / 2);
      else if (filter === 4) value += paeth(left, up, upLeft);
      else if (filter !== 0) throw new Error(`Unsupported PNG filter: ${filter}`);

      rgba[outputOffset + x] = value & 0xff;
    }
  }

  return { width, height, rgba };
}

function paeth(left, up, upLeft) {
  const estimate = left + up - upLeft;
  const distanceLeft = Math.abs(estimate - left);
  const distanceUp = Math.abs(estimate - up);
  const distanceUpLeft = Math.abs(estimate - upLeft);

  if (distanceLeft <= distanceUp && distanceLeft <= distanceUpLeft) return left;
  if (distanceUp <= distanceUpLeft) return up;
  return upLeft;
}

function visibleSquare(image, alphaThreshold = 8) {
  let minX = image.width;
  let minY = image.height;
  let maxX = -1;
  let maxY = -1;

  for (let y = 0; y < image.height; y += 1) {
    for (let x = 0; x < image.width; x += 1) {
      const alpha = image.rgba[(y * image.width + x) * 4 + 3];
      if (alpha > alphaThreshold) {
        minX = Math.min(minX, x);
        minY = Math.min(minY, y);
        maxX = Math.max(maxX, x);
        maxY = Math.max(maxY, y);
      }
    }
  }

  if (maxX < 0 || maxY < 0) {
    throw new Error("Source icon has no visible pixels.");
  }

  const visibleWidth = maxX - minX + 1;
  const visibleHeight = maxY - minY + 1;
  const side = Math.max(visibleWidth, visibleHeight);
  const centerX = (minX + maxX) / 2;
  const centerY = (minY + maxY) / 2;
  const left = Math.max(0, Math.min(image.width - side, Math.round(centerX - side / 2)));
  const top = Math.max(0, Math.min(image.height - side, Math.round(centerY - side / 2)));

  return { left, top, side };
}

function resizePng(image, crop, size) {
  const rgba = Buffer.alloc(size * size * 4);
  const scale = crop.side / size;

  for (let y = 0; y < size; y += 1) {
    for (let x = 0; x < size; x += 1) {
      const sourceX = crop.left + (x + 0.5) * scale - 0.5;
      const sourceY = crop.top + (y + 0.5) * scale - 0.5;
      const pixel = sampleBilinear(image, sourceX, sourceY);
      const offset = (y * size + x) * 4;
      rgba[offset] = pixel.r;
      rgba[offset + 1] = pixel.g;
      rgba[offset + 2] = pixel.b;
      rgba[offset + 3] = pixel.a;
    }
  }

  return encodePng(size, size, rgba);
}

function sampleBilinear(image, x, y) {
  const x0 = Math.max(0, Math.min(image.width - 1, Math.floor(x)));
  const y0 = Math.max(0, Math.min(image.height - 1, Math.floor(y)));
  const x1 = Math.max(0, Math.min(image.width - 1, x0 + 1));
  const y1 = Math.max(0, Math.min(image.height - 1, y0 + 1));
  const tx = x - x0;
  const ty = y - y0;
  const top = mixPixels(getPixel(image, x0, y0), getPixel(image, x1, y0), tx);
  const bottom = mixPixels(getPixel(image, x0, y1), getPixel(image, x1, y1), tx);

  return mixPixels(top, bottom, ty);
}

function getPixel(image, x, y) {
  const offset = (y * image.width + x) * 4;
  return {
    r: image.rgba[offset],
    g: image.rgba[offset + 1],
    b: image.rgba[offset + 2],
    a: image.rgba[offset + 3],
  };
}

function mixPixels(a, b, t) {
  return {
    r: Math.round(a.r + (b.r - a.r) * t),
    g: Math.round(a.g + (b.g - a.g) * t),
    b: Math.round(a.b + (b.b - a.b) * t),
    a: Math.round(a.a + (b.a - a.a) * t),
  };
}

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

function crc32(buffer) {
  let crc = 0xffffffff;
  for (const byte of buffer) {
    crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
  }
  return (crc ^ 0xffffffff) >>> 0;
}

function chunk(type, payload) {
  const typeBytes = Buffer.from(type);
  const out = Buffer.alloc(12 + payload.length);
  out.writeUInt32BE(payload.length, 0);
  typeBytes.copy(out, 4);
  payload.copy(out, 8);
  out.writeUInt32BE(crc32(Buffer.concat([typeBytes, payload])), out.length - 4);
  return out;
}

function encodePng(width, height, rgba) {
  const header = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
  const ihdr = Buffer.alloc(13);
  ihdr.writeUInt32BE(width, 0);
  ihdr.writeUInt32BE(height, 4);
  ihdr[8] = 8;
  ihdr[9] = 6;

  const raw = Buffer.alloc((width * 4 + 1) * height);
  for (let y = 0; y < height; y += 1) {
    const rowStart = y * (width * 4 + 1);
    raw[rowStart] = 0;
    rgba.copy(raw, rowStart + 1, y * width * 4, (y + 1) * width * 4);
  }

  return Buffer.concat([
    header,
    chunk("IHDR", ihdr),
    chunk("IDAT", deflateSync(raw, { level: 9 })),
    chunk("IEND", Buffer.alloc(0)),
  ]);
}

function makeIco(images) {
  const header = Buffer.alloc(6);
  header.writeUInt16LE(0, 0);
  header.writeUInt16LE(1, 2);
  header.writeUInt16LE(images.length, 4);

  const entries = Buffer.alloc(images.length * 16);
  let offset = 6 + images.length * 16;

  images.forEach((image, index) => {
    const entry = index * 16;
    entries[entry] = image.size >= 256 ? 0 : image.size;
    entries[entry + 1] = image.size >= 256 ? 0 : image.size;
    entries[entry + 2] = 0;
    entries[entry + 3] = 0;
    entries.writeUInt16LE(1, entry + 4);
    entries.writeUInt16LE(32, entry + 6);
    entries.writeUInt32LE(image.png.length, entry + 8);
    entries.writeUInt32LE(offset, entry + 12);
    offset += image.png.length;
  });

  return Buffer.concat([header, entries, ...images.map((image) => image.png)]);
}

mkdirSync(iconsDir, { recursive: true });

const source = decodePng(readFileSync(sourcePath));
const crop = visibleSquare(source);
const rendered = new Map(
  [...new Set([...pngSizes, ...icoSizes])].map((size) => [size, resizePng(source, crop, size)]),
);

writeFileSync(join(iconsDir, "32x32.png"), rendered.get(32));
writeFileSync(join(iconsDir, "64x64.png"), rendered.get(64));
writeFileSync(join(iconsDir, "128x128.png"), rendered.get(128));
writeFileSync(join(iconsDir, "icon.png"), rendered.get(512));
writeFileSync(join(iconsDir, "icon.ico"), makeIco(icoSizes.map((size) => ({ size, png: rendered.get(size) }))));

console.log(`Read ${join("src-tauri", "icons", "GitCat_org.png")}`);
console.log(`Cropped visible icon area to ${crop.side}x${crop.side} at ${crop.left},${crop.top}`);
console.log(`Wrote ${join("src-tauri", "icons", "32x32.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "64x64.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "128x128.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "icon.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "icon.ico")} (${icoSizes.length} sizes)`);
