#!/usr/bin/env python3
# Substitui o default/gimp-xcf2png do Bugz (que roda o GIMP em modo batch): le os .xcf das fases e grava
# as camadas "Background", "Ladders" e "Entities" como <nome>.xcf.png, <nome>.xcf-ladders.png e
# <nome>.xcf-entities.png, como o gimp-file-save daquele script (so os pixels da camada, no tamanho dela).
# As fases sao imagens indexadas: as cores saem ja expandidas da paleta (RGB, ou RGBA se a camada tem
# alfa), que e o que o png2inc do proprio Bugz faz com o PNG indexado do GIMP antes de converter em .inc.
# So biblioteca padrao do Python.
#
#   bugz-xcf2png.py <pasta com os .xcf> <pasta de saida>
#
# Formato XCF (versoes 0 a 3, ponteiros de 32 bits): https://developer.gimp.org/core/standards/xcf/
# Copyright (C) 2026 FredyGames. GPL-3.0-or-later, como o Bugz.
import os
import struct
import sys
import zlib

PROP_END, PROP_COLORMAP, PROP_COMPRESSION = 0, 1, 17
LAYERS = (("Background", ".xcf.png"), ("Ladders", ".xcf-ladders.png"), ("Entities", ".xcf-entities.png"))


class Reader:
    def __init__(self, data):
        self.d = data

    def u32(self, at):
        return struct.unpack_from(">I", self.d, at)[0]

    def props(self, at):
        out = {}
        while True:
            kind, size = struct.unpack_from(">II", self.d, at)
            at += 8
            if kind == PROP_END:
                return out, at
            out[kind] = self.d[at:at + size]
            at += size

    def string(self, at):
        n = self.u32(at)
        return self.d[at + 4:at + 4 + n].rstrip(b"\0").decode("utf-8"), at + 4 + n

    def pointers(self, at):
        out = []
        while True:
            p = self.u32(at)
            at += 4
            if p == 0:
                return out, at
            out.append(p)


def rle_decode(d, at, count, bpp):
    """Decodifica um ladrilho RLE do XCF (canais em planos separados) -> bytes intercalados."""
    planes = []
    for _ in range(bpp):
        plane = bytearray()
        while len(plane) < count:
            op = d[at]
            at += 1
            if op >= 128:
                if op == 128:
                    n = struct.unpack_from(">H", d, at)[0]
                    at += 2
                else:
                    n = 256 - op
                plane += d[at:at + n]
                at += n
            else:
                if op == 127:
                    n = struct.unpack_from(">H", d, at)[0]
                    at += 2
                else:
                    n = op + 1
                plane += bytes([d[at]]) * n
                at += 1
        if len(plane) != count:
            raise ValueError("RLE fora do tamanho do ladrilho")
        planes.append(plane)
    out = bytearray(count * bpp)
    for c in range(bpp):
        out[c::bpp] = planes[c]
    return out


def read_layer(r, at, compression):
    width, height, _type = struct.unpack_from(">III", r.d, at)
    name, at = r.string(at + 12)
    _props, at = r.props(at)
    hierarchy = r.u32(at)
    hw, hh, bpp = struct.unpack_from(">III", r.d, hierarchy)
    levels, _ = r.pointers(hierarchy + 12)
    lw, lh = struct.unpack_from(">II", r.d, levels[0])
    tiles, _ = r.pointers(levels[0] + 8)
    assert (hw, hh) == (width, height) == (lw, lh), name
    pixels = bytearray(width * height * bpp)
    tx_count = (width + 63) // 64
    for i, tile in enumerate(tiles):
        tx, ty = i % tx_count, i // tx_count
        x0, y0 = tx * 64, ty * 64
        tw, th = min(64, width - x0), min(64, height - y0)
        if compression == 0:
            data = r.d[tile:tile + tw * th * bpp]
        elif compression == 1:
            data = rle_decode(r.d, tile, tw * th, bpp)
        else:
            raise ValueError(f"compressao {compression} nao suportada")
        for y in range(th):
            row = (y0 + y) * width + x0
            pixels[row * bpp:(row + tw) * bpp] = data[y * tw * bpp:(y + 1) * tw * bpp]
    return name, width, height, bpp, pixels


def write_png(path, width, height, bpp, pixels):
    color = {3: 2, 4: 6}[bpp]  # RGB, RGBA
    raw = b"".join(b"\0" + bytes(pixels[y * width * bpp:(y + 1) * width * bpp]) for y in range(height))

    def chunk(kind, body):
        return struct.pack(">I", len(body)) + kind + body + struct.pack(">I", zlib.crc32(kind + body) & 0xFFFFFFFF)

    png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, color, 0, 0, 0))
    png += chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")
    with open(path, "wb") as f:
        f.write(png)


def convert(src, dst_dir):
    with open(src, "rb") as f:
        r = Reader(f.read())
    if not r.d.startswith(b"gimp xcf ") or r.d[13] != 0:
        raise ValueError(f"{src}: XCF com ponteiros de 64 bits (v11+) nao suportado")
    width, height, base = struct.unpack_from(">III", r.d, 14)
    props, at = r.props(26)
    compression = props.get(PROP_COMPRESSION, b"\0")[0]
    if base == 2:  # indexada: u32 numero de cores + RGB
        cmap = props[PROP_COLORMAP]
        palette = [bytes(cmap[4 + i * 3:7 + i * 3]) for i in range(struct.unpack_from(">I", cmap)[0])]
    elif base != 0:
        raise ValueError(f"{src}: imagem em tons de cinza nao suportada")
    layers, at = r.pointers(at)
    found = {}
    for p in layers:
        name, w, h, bpp, pixels = read_layer(r, p, compression)
        if base == 2:
            rgb = bytearray()
            for i in range(0, len(pixels), bpp):
                rgb += palette[pixels[i]] + (bytes([pixels[i + 1]]) if bpp == 2 else b"")
            pixels, bpp = rgb, bpp + 2
        found.setdefault(name, (w, h, bpp, pixels))
    stem = os.path.basename(src)[:-4]
    for name, suffix in LAYERS:
        if name not in found:
            raise ValueError(f"{src}: falta a camada {name}")
        write_png(os.path.join(dst_dir, stem + suffix), *found[name])


def main():
    src_dir, dst_dir = sys.argv[1], sys.argv[2]
    os.makedirs(dst_dir, exist_ok=True)
    for name in sorted(os.listdir(src_dir)):
        if name.endswith(".xcf"):
            convert(os.path.join(src_dir, name), dst_dir)


if __name__ == "__main__":
    main()
