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

Why:
- Premium avatars are served privately via the API (/api/avatars/*/image)
- The shop still needs marketing previews visible to everyone
- These previews are downscaled thumbnails (not full-size images)

No external dependencies: pure Python PNG decode/encode (RGBA, 8-bit, no interlace).
"""

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 downscale_rgba_box(
    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 % out_w != 0 or in_h % out_h != 0:
        raise ValueError(
            f"Only integer downscale supported (in={in_w}x{in_h} out={out_w}x{out_h})."
        )
    fx = in_w // out_w
    fy = in_h // out_h
    if fx != fy:
        # Keep it simple: require uniform scale for now.
        raise ValueError("Non-uniform scale not supported.")
    f = fx

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

    for oy in range(out_h):
        iy0 = oy * f
        for ox in range(out_w):
            ix0 = ox * f
            sr = sg = sb = sa = 0
            for dy in range(f):
                row_start = (iy0 + dy) * in_stride + ix0 * 4
                for dx in range(f):
                    i = row_start + dx * 4
                    sr += in_rgba[i + 0]
                    sg += in_rgba[i + 1]
                    sb += in_rgba[i + 2]
                    sa += in_rgba[i + 3]
            count = f * f
            o = oy * out_stride + ox * 4
            out[o + 0] = sr // count
            out[o + 1] = sg // count
            out[o + 2] = sb // count
            out[o + 3] = sa // count
    return bytes(out)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--in-dir",
        default="rollerlogic-api/private/avatars/cool",
        help="Input directory containing the real pack images (01.png..).",
    )
    parser.add_argument(
        "--out-dir",
        default="rollerlogic-mobile/public/avatars/previews/cool",
        help="Output directory for public thumbnails.",
    )
    parser.add_argument(
        "--size",
        type=int,
        default=128,
        help="Thumbnail size (square). Must divide input size (e.g. 2560 -> 128).",
    )
    parser.add_argument(
        "--count",
        type=int,
        default=4,
        help="How many previews to generate (starting at 01).",
    )
    args = parser.parse_args()

    in_dir = Path(args.in_dir)
    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    for i in range(1, args.count + 1):
        name = f"{i:02d}"
        src = in_dir / f"{name}.png"
        dst = out_dir / f"{name}.png"

        w, h, rgba = decode_png_rgba8(src)
        thumb = downscale_rgba_box(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})")

    return 0


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

