#!/usr/bin/env python3
"""
Generate icon PNGs/ICO for the desktop installer using the high-resolution Raven artwork.

The script reads the square logo (`logo-raven-fund-azul.png`) and resizes it to the
target sizes with a simple bilinear filter implemented with the Python standard library,
avoiding additional dependencies.
"""

from __future__ import annotations

import math
import struct
import zlib
from binascii import crc32
from pathlib import Path

ICON_DIR = Path(__file__).resolve().parents[1] / "src-tauri" / "icons"
BASE_IMAGE = ICON_DIR / "logo-raven-fund-azul.png"
TARGET_SIZES = [32, 64, 128, 256, 512]


def read_png(path: Path) -> tuple[int, int, list[list[tuple[int, int, int, int]]]]:
  data = path.read_bytes()
  if not data.startswith(b"\x89PNG\r\n\x1a\n"):
    raise ValueError(f"{path} is not a PNG")
  pos = 8
  width = height = bit_depth = color_type = None
  compressed_parts = []
  while pos < len(data):
    length = struct.unpack(">I", data[pos : pos + 4])[0]
    pos += 4
    ctype = data[pos : pos + 4]
    pos += 4
    chunk = data[pos : pos + length]
    pos += length
    pos += 4  # CRC
    if ctype == b"IHDR":
      width, height, bit_depth, color_type, _, _, _ = struct.unpack(">IIBBBBB", chunk)
      if bit_depth != 8 or color_type not in (2, 6):
        raise ValueError("Only 8-bit RGB/RGBA PNGs are supported")
    elif ctype == b"IDAT":
      compressed_parts.append(chunk)
    elif ctype == b"IEND":
      break
  if width is None or height is None or bit_depth is None or color_type is None:
    raise ValueError("PNG missing IHDR chunk")

  raw = zlib.decompress(b"".join(compressed_parts))
  bpp = 4 if color_type == 6 else 3
  stride = width * bpp
  rows = []
  idx = 0
  prev = bytearray(stride)
  for _ in range(height):
    filter_type = raw[idx]
    idx += 1
    row = bytearray(raw[idx : idx + stride])
    idx += stride
    if filter_type == 1:
      for i in range(stride):
        left = row[i - bpp] if i >= bpp else 0
        row[i] = (row[i] + left) & 0xFF
    elif filter_type == 2:
      for i in range(stride):
        row[i] = (row[i] + prev[i]) & 0xFF
    elif filter_type == 3:
      for i in range(stride):
        left = row[i - bpp] if i >= bpp else 0
        up = prev[i]
        row[i] = (row[i] + ((left + up) // 2)) & 0xFF
    elif filter_type == 4:
      for i in range(stride):
        left = row[i - bpp] if i >= bpp else 0
        up = prev[i]
        up_left = prev[i - bpp] if i >= bpp else 0
        p = left + up - up_left
        pa = abs(p - left)
        pb = abs(p - up)
        pc = abs(p - up_left)
        if pa <= pb and pa <= pc:
          pr = left
        elif pb <= pc:
          pr = up
        else:
          pr = up_left
        row[i] = (row[i] + pr) & 0xFF
    elif filter_type not in (0,):
      raise ValueError(f"Unsupported PNG filter type {filter_type}")
    rows.append(bytes(row))
    prev[:] = row

  pixels: list[list[tuple[int, int, int, int]]] = []
  for row in rows:
    if color_type == 6:
      pixels.append([tuple(row[i : i + 4]) for i in range(0, len(row), 4)])
    else:
      pixels.append([tuple(row[i : i + 3] + b"\xff") for i in range(0, len(row), 3)])
  return width, height, pixels


def write_png(path: Path, width: int, height: int, pixels: list[list[tuple[int, int, int, int]]]) -> None:
  raw = bytearray()
  for row in pixels:
    raw.append(0)  # filter type 0
    for r, g, b, a in row:
      raw.extend((r & 0xFF, g & 0xFF, b & 0xFF, a & 0xFF))
  compressed = zlib.compress(raw, level=9)

  def chunk(name: bytes, payload: bytes) -> bytes:
    return (
      struct.pack(">I", len(payload))
      + name
      + payload
      + struct.pack(">I", crc32(name + payload) & 0xFFFFFFFF)
    )

  ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
  out = bytearray(b"\x89PNG\r\n\x1a\n")
  out += chunk(b"IHDR", ihdr)
  out += chunk(b"IDAT", compressed)
  out += chunk(b"IEND", b"")
  path.write_bytes(out)


def bilinear_sample(pixels: list[list[tuple[int, int, int, int]]], x: float, y: float) -> tuple[int, int, int, int]:
  height = len(pixels)
  width = len(pixels[0])
  x = min(max(x, 0.0), width - 1.0)
  y = min(max(y, 0.0), height - 1.0)
  x0 = int(math.floor(x))
  y0 = int(math.floor(y))
  x1 = min(x0 + 1, width - 1)
  y1 = min(y0 + 1, height - 1)
  dx = x - x0
  dy = y - y0

  def lerp(a: float, b: float, t: float) -> float:
    return a + (b - a) * t

  result = []
  for channel in range(4):
    c00 = pixels[y0][x0][channel]
    c10 = pixels[y0][x1][channel]
    c01 = pixels[y1][x0][channel]
    c11 = pixels[y1][x1][channel]
    top = lerp(c00, c10, dx)
    bottom = lerp(c01, c11, dx)
    result.append(int(round(lerp(top, bottom, dy))))
  return tuple(result)


def resize_image(pixels: list[list[tuple[int, int, int, int]]], target: int) -> list[list[tuple[int, int, int, int]]]:
  src_height = len(pixels)
  src_width = len(pixels[0])
  scale = min(target / src_width, target / src_height)
  dest_width = max(1, int(round(src_width * scale)))
  dest_height = max(1, int(round(src_height * scale)))
  offset_x = (target - dest_width) // 2
  offset_y = (target - dest_height) // 2

  background = (0, 0, 0, 0)
  canvas = [[background for _ in range(target)] for _ in range(target)]

  for dy in range(dest_height):
    src_y = (dy + 0.5) / scale - 0.5
    for dx in range(dest_width):
      src_x = (dx + 0.5) / scale - 0.5
      canvas[offset_y + dy][offset_x + dx] = bilinear_sample(pixels, src_x, src_y)
  return canvas


def build_ico(output: Path, png_paths: list[Path]) -> None:
  entries = []
  offset = 6 + 16 * len(png_paths)
  for path in png_paths:
    data = path.read_bytes()
    width, height, _ = read_png(path)
    entries.append(
      {
        "width": width if width < 256 else 0,
        "height": height if height < 256 else 0,
        "size": len(data),
        "offset": offset,
        "payload": data,
      }
    )
    offset += len(data)

  header = struct.pack("<HHH", 0, 1, len(entries))
  body = bytearray(header)
  for entry in entries:
    body.extend(
      struct.pack(
        "<BBBBHHII",
        entry["width"],
        entry["height"],
        0,
        0,
        1,
        32,
        entry["size"],
        entry["offset"],
      )
    )
  for entry in entries:
    body.extend(entry["payload"])
  output.write_bytes(body)


def main() -> None:
  width, height, pixels = read_png(BASE_IMAGE)
  if width != height:
    raise ValueError("Base icon must be square")

  generated: list[Path] = []
  for size in TARGET_SIZES:
    resized = resize_image(pixels, size)
    out_path = ICON_DIR / f"icon-{size}.png"
    write_png(out_path, size, size, resized)
    generated.append(out_path)
    print(f"Generated {out_path} ({size}x{size})")

  largest = max(generated, key=lambda p: int(p.stem.split("-")[-1]))
  (ICON_DIR / "icon.png").write_bytes(largest.read_bytes())

  ico_sources = sorted(
    [p for p in generated if int(p.stem.split("-")[-1]) <= 256],
    key=lambda p: int(p.stem.split("-")[-1]),
  )
  build_ico(ICON_DIR / "icon.ico", ico_sources)
  print("icon.ico rebuilt.")


if __name__ == "__main__":
  main()

