#!/usr/bin/env python3
"""Pack icon.png into a multi-size icon.ico used by the Windows resource compiler.

ICO format note:
    Vista+ allows PNG-encoded frames inside ICO files, BUT the Microsoft
    resource compiler (rc.exe) only reliably accepts PNG payloads for the
    256x256 frame. PNG payloads for 16/32/48/64/128 are silently dropped
    (or worse — rc.exe writes a resource section Explorer can't decode and
    the EXE shows the generic icon). So we encode small sizes as DIB
    (BITMAPINFOHEADER + BGRA pixels + AND-mask) and keep PNG only for 256.

Requires Pillow. We use it to decode the source PNG and resample.

    python3 -m venv .venv && .venv/bin/pip install Pillow
    .venv/bin/python3 resources/build_ico.py
"""
import io
import struct
import sys
from pathlib import Path

try:
    from PIL import Image
except ImportError:
    sys.exit("Pillow required: pip install Pillow")

DIB_SIZES = [16, 32, 48, 64, 128]   # BITMAPINFOHEADER + BGRA
PNG_SIZES = [256]                    # PNG payload (preserves alpha cleanly at large size)
HERE = Path(__file__).parent
SRC = HERE / "icon.png"


def encode_dib(img: Image.Image, size: int) -> bytes:
    """Encode an RGBA image as a DIB-format ICO frame.

    Layout: BITMAPINFOHEADER (40 bytes), then BGRA pixels bottom-up, then
    a 1-bit AND-mask (also bottom-up, row-padded to 4 bytes). The header
    declares double the actual height so Windows knows the AND-mask is
    appended — this is the (counter-intuitive but mandatory) ICO
    convention; the file is otherwise an ordinary 32bpp BMP minus the
    14-byte BITMAPFILEHEADER.
    """
    img = img.convert("RGBA").resize((size, size), Image.LANCZOS)
    pixels = img.load()

    # 32bpp + alpha — AND mask can be all zero (fully opaque-from-mask;
    # the per-pixel alpha is what Windows actually composites). Still
    # required structurally.
    bgra_rows = []
    for y in range(size - 1, -1, -1):  # bottom-up
        row = bytearray()
        for x in range(size):
            r, g, b, a = pixels[x, y]
            row += bytes((b, g, r, a))
        bgra_rows.append(bytes(row))
    bgra = b"".join(bgra_rows)

    mask_row_bytes = ((size + 31) // 32) * 4   # 4-byte aligned
    mask = b"\x00" * (mask_row_bytes * size)

    header = struct.pack(
        "<IiiHHIIiiII",
        40,            # biSize
        size,          # biWidth
        size * 2,      # biHeight (double — XOR + AND mask)
        1,             # biPlanes
        32,            # biBitCount
        0,             # biCompression = BI_RGB
        len(bgra) + len(mask),  # biSizeImage
        0, 0, 0, 0,    # ppm, colors used, colors important
    )
    return header + bgra + mask


def encode_png(img: Image.Image, size: int) -> bytes:
    img = img.convert("RGBA").resize((size, size), Image.LANCZOS)
    buf = io.BytesIO()
    img.save(buf, format="PNG", optimize=True)
    return buf.getvalue()


def main() -> None:
    if not SRC.exists():
        sys.exit(f"missing {SRC}")
    src = Image.open(SRC)

    frames = []
    for sz in DIB_SIZES:
        frames.append((sz, "dib", encode_dib(src, sz)))
    for sz in PNG_SIZES:
        frames.append((sz, "png", encode_png(src, sz)))

    out = bytearray()
    # ICONDIR: reserved(2) + type=1(2) + count(2)
    out += struct.pack("<HHH", 0, 1, len(frames))

    header_size = 6 + 16 * len(frames)
    offset = header_size
    for sz, _kind, payload in frames:
        w = 0 if sz == 256 else sz
        h = 0 if sz == 256 else sz
        out += struct.pack(
            "<BBBBHHII",
            w, h,
            0,    # colorCount
            0,    # reserved
            1,    # planes
            32,   # bitCount
            len(payload),
            offset,
        )
        offset += len(payload)

    for _, _, payload in frames:
        out += payload

    (HERE / "icon.ico").write_bytes(out)
    summary = ", ".join(f"{sz}{'·png' if k=='png' else ''}" for sz, k, _ in frames)
    print(f"wrote {HERE / 'icon.ico'} ({len(out)} bytes; frames: {summary})")


if __name__ == "__main__":
    main()
