#!/usr/bin/env python3
"""
Generate small public preview thumbnails for premium ball-skin packs.

Premium ball skins are served privately via the API (/api/ball-skins/:id/image),
but the shop should display small marketing thumbnails even for non-owners.

This script downscales the real private PNGs into public thumbnails under:
  rollerlogic-mobile/public/images/billes/previews/<skinId>.png

No external deps: pure Python PNG decode/encode (RGBA, 8-bit, non-interlaced).
"""

from __future__ import annotations

import argparse
import struct
import zlib
from pathlib import Path
from typing import Iterable


PNG_SIG = b"\x89PNG\r\n\x1a\n"


def _crc(chunk_type: bytes, data: bytes) -> int:
    return zlib.crc32(chunk_type + data) & 0xFFFFFFFF


def _read_chunks(png_bytes: bytes) -> Iterable[tuple[bytes, bytes]]:
    if not png_bytes.startswith(PNG_SIG):
        raise ValueError("Not a PNG file (bad signature).")
    i = len(PNG_SIG)
    n = len(png_bytes)
    while i < n:
        if i + 8 > n:
            raise ValueError("Truncated PNG.")
        length = struct.unpack(">I", png_bytes[i : i + 4])[0]
        ctype = png_bytes[i + 4 : i + 8]
        i += 8
        if i + length + 4 > n:
            raise ValueError("Truncated PNG chunk.")
        data = png_bytes[i : i + length]
        i += length
        _chunk_crc = struct.unpack(">I", png_bytes[i : i + 4])[0]
        i += 4
        yield (ctype, data)


def _unfilter_scanlines(raw: bytes, width: int, height: int, bpp: int) -> bytes:
    stride = width * bpp
    out = bytearray(height * stride)
    in_i = 0
    for y in range(height):
        filter_type = raw[in_i]
        in_i += 1
        row = raw[in_i : in_i + stride]
        in_i += stride
        out_row_start = y * stride
        prev_row_start = (y - 1) * stride

        if filter_type == 0:  # None
            out[out_row_start : out_row_start + stride] = row
            continue

        for x in range(stride):
            a = out[out_row_start + x - bpp] if x >= bpp else 0
            b = out[prev_row_start + x] if y > 0 else 0
            c = out[prev_row_start + x - bpp] if (y > 0 and x >= bpp) else 0
            f = row[x]

            if filter_type == 1:  # Sub
                val = (f + a) & 0xFF
            elif filter_type == 2:  # Up
                val = (f + b) & 0xFF
            elif filter_type == 3:  # Average
                val = (f + ((a + b) // 2)) & 0xFF
            elif filter_type == 4:  # Paeth
                p = a + b - c
                pa = abs(p - a)
                pb = abs(p - b)
                pc = abs(p - c)
                pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c)
                val = (f + pr) & 0xFF
            else:
                raise ValueError(f"Unsupported PNG filter type: {filter_type}")

            out[out_row_start + x] = val
    return bytes(out)


def decode_png_rgba8(path: Path) -> tuple[int, int, bytes]:
    data = path.read_bytes()
    width = height = None
    bit_depth = color_type = interlace = None
    idat = bytearray()

    for ctype, cdata in _read_chunks(data):
        if ctype == b"IHDR":
            width, height, bit_depth, color_type, _cm, _fm, interlace = struct.unpack(
                ">IIBBBBB", cdata
            )
        elif ctype == b"IDAT":
            idat.extend(cdata)
        elif ctype == b"IEND":
            break

    if width is None or height is None:
        raise ValueError("Missing IHDR.")
    if bit_depth != 8 or color_type != 6:
        raise ValueError(
            f"Unsupported PNG format: bit_depth={bit_depth} color_type={color_type} (need RGBA8)"
        )
    if interlace != 0:
        raise ValueError("Interlaced PNG not supported.")

    raw = zlib.decompress(bytes(idat))
    rgba = _unfilter_scanlines(raw, width, height, bpp=4)
    return width, height, rgba


def encode_png_rgba8(width: int, height: int, rgba: bytes) -> bytes:
    stride = width * 4
    if len(rgba) != height * stride:
        raise ValueError("Invalid RGBA buffer length.")

    # No filters (0) for simplicity.
    scan = bytearray()
    for y in range(height):
        scan.append(0)
        start = y * stride
        scan.extend(rgba[start : start + stride])

    compressed = zlib.compress(bytes(scan), level=9)

    def chunk(ctype: bytes, cdata: bytes) -> bytes:
        return (
            struct.pack(">I", len(cdata))
            + ctype
            + cdata
            + struct.pack(">I", _crc(ctype, cdata))
        )

    ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
    return PNG_SIG + chunk(b"IHDR", ihdr) + chunk(b"IDAT", compressed) + chunk(b"IEND", b"")


def resize_rgba_bilinear(in_w: int, in_h: int, in_rgba: bytes, out_w: int, out_h: int) -> bytes:
    if out_w <= 0 or out_h <= 0:
        raise ValueError("Invalid output size.")
    if in_w <= 0 or in_h <= 0:
        raise ValueError("Invalid input size.")

    out = bytearray(out_w * out_h * 4)
    in_stride = in_w * 4
    out_stride = out_w * 4

    # Map pixel centers.
    sx = in_w / out_w
    sy = in_h / out_h

    for oy in range(out_h):
        fy = (oy + 0.5) * sy - 0.5
        y0 = int(fy)
        y1 = y0 + 1
        wy = fy - y0
        if y0 < 0:
            y0 = 0
            y1 = 0
            wy = 0.0
        if y1 >= in_h:
            y1 = in_h - 1
        row0 = y0 * in_stride
        row1 = y1 * in_stride
        for ox in range(out_w):
            fx = (ox + 0.5) * sx - 0.5
            x0 = int(fx)
            x1 = x0 + 1
            wx = fx - x0
            if x0 < 0:
                x0 = 0
                x1 = 0
                wx = 0.0
            if x1 >= in_w:
                x1 = in_w - 1
            i00 = row0 + x0 * 4
            i10 = row0 + x1 * 4
            i01 = row1 + x0 * 4
            i11 = row1 + x1 * 4

            w00 = (1.0 - wx) * (1.0 - wy)
            w10 = wx * (1.0 - wy)
            w01 = (1.0 - wx) * wy
            w11 = wx * wy

            o = oy * out_stride + ox * 4
            for c in range(4):
                v = (
                    in_rgba[i00 + c] * w00
                    + in_rgba[i10 + c] * w10
                    + in_rgba[i01 + c] * w01
                    + in_rgba[i11 + c] * w11
                )
                out[o + c] = 0 if v < 0 else (255 if v > 255 else int(v + 0.5))

    return bytes(out)


def resolve_ball_skin_source(root: Path, skin_id: str) -> Path:
    # skin_id formats:
    # - elementaire_1
    # - halloween_3
    # - varie_10
    # - design_unique_1 (not used here, but supported)
    parts = skin_id.split("_")
    if len(parts) == 2:
        category = parts[0]
        number = parts[1]
    elif len(parts) >= 3:
        category = f"{parts[0]}_{parts[1]}"
        number = parts[2]
    else:
        raise ValueError(f"Invalid skin_id: {skin_id}")

    if category == "elementaire":
        category = "elementaires"
    return root / category / f"bille_{number}.png"


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--in-root",
        default="rollerlogic-api/private/ball-skins",
        help="Input root directory containing private ball-skin images.",
    )
    parser.add_argument(
        "--out-root",
        default="rollerlogic-mobile/public/images/billes/previews",
        help="Output directory for public thumbnails.",
    )
    parser.add_argument(
        "--size",
        type=int,
        default=96,
        help="Thumbnail size (square).",
    )
    parser.add_argument(
        "--skins",
        nargs="*",
        default=[
            # Base (design_unique)
            *[f"design_unique_{i}" for i in range(1, 11)],
            # Packs déjà disponibles
            *[f"elementaire_{i}" for i in range(1, 11)],
            *[f"halloween_{i}" for i in range(1, 11)],
            *[f"varie_{i}" for i in range(1, 21)],
        ],
        help="Skin ids to generate thumbnails for.",
    )
    args = parser.parse_args()

    in_root = Path(args.in_root)
    out_root = Path(args.out_root)
    out_root.mkdir(parents=True, exist_ok=True)

    for skin_id in args.skins:
        src = resolve_ball_skin_source(in_root, skin_id)
        if not src.exists():
            raise FileNotFoundError(src)
        dst = out_root / f"{skin_id}.png"

        w, h, rgba = decode_png_rgba8(src)
        thumb = resize_rgba_bilinear(w, h, rgba, args.size, args.size)
        dst.write_bytes(encode_png_rgba8(args.size, args.size, thumb))
        print(f"wrote {dst} ({args.size}x{args.size}) <- {src.name} ({w}x{h})")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
