#!/usr/bin/env python3
"""
mkatlas.py — упаковщик атласов спрайтов Sprinter (.atl, вариант II —
docs/sprite-api-design.md §3.1).

Формат файла (== образ EMM-страницы, файл-офсет == W0-адрес):
  0x000  'S','P','A','1', count u8, резерв ×3
  0x008  резерв (0x38-0x3A и 0x66-0x67 — под патч ISR-стаба загрузчиком)
  0x068  каталог: count × 8 Б {offset u16 LE, fw u8, fh u8, nx u8, ny u8,
         резерв u16} — offset указывает на getimage-ЗАГОЛОВОК ленты
  0x100  ленты подряд: u16 w=fw*nx, u16 h=fh*ny (LE) + пиксели построчно

Вход — список лент:
  img.png:FWxFH            индексированный PNG (mode P/L; байты как есть,
                           прозрачность = индекс 0xFF); сетка nx,ny из
                           размеров картинки
  img.raw:FWxFH:NXxNY      сырые байты (fw*nx)×(fh*ny), построчно

Пример:
  mkatlas.py sprites.atl hero.png:16x16 coin.png:16x16 boss.raw:24x24:2x2

Ограничения: count ≤ 19, файл ≤ 16384 Б, кадр ≤ 64×64.
"""
import os
import sys

HDR_SIZE  = 0x100
DIR_OFF   = 0x68
MAX_ENTRY = (HDR_SIZE - DIR_OFF) // 8      # 19
PAGE_SIZE = 16384


def die(msg):
    sys.exit(f"mkatlas: {msg}")


def load_png(path):
    from PIL import Image
    im = Image.open(path)
    if im.mode not in ("P", "L"):
        die(f"{path}: нужен индексированный PNG (mode P/L), не {im.mode}")
    return im.width, im.height, bytes(im.tobytes())


def parse_spec(spec):
    parts = spec.split(":")
    if len(parts) < 2:
        die(f"'{spec}': нужен вид file:FWxFH[:NXxNY]")
    path = parts[0]
    fw, fh = (int(v) for v in parts[1].lower().split("x"))
    if not (1 <= fw <= 64 and 1 <= fh <= 64):
        die(f"{path}: кадр {fw}x{fh} вне 1..64 (контракт спрайтов)")

    if path.lower().endswith(".png"):
        if len(parts) != 2:
            die(f"{path}: для PNG сетка берётся из размеров картинки")
        w, h, pix = load_png(path)
    else:
        if len(parts) != 3:
            die(f"{path}: для raw обязательна сетка :NXxNY")
        nx, ny = (int(v) for v in parts[2].lower().split("x"))
        w, h = fw * nx, fh * ny
        pix = open(path, "rb").read()
        if len(pix) != w * h:
            die(f"{path}: {len(pix)} байт, ожидалось {w}x{h}={w * h}")

    if w % fw or h % fh:
        die(f"{path}: {w}x{h} не делится на кадр {fw}x{fh}")
    nx, ny = w // fw, h // fh
    if not (1 <= nx <= 255 and 1 <= ny <= 255):
        die(f"{path}: сетка {nx}x{ny} вне 1..255")
    return dict(path=path, fw=fw, fh=fh, nx=nx, ny=ny, w=w, h=h, pix=pix)


def main():
    if len(sys.argv) < 3:
        sys.exit(__doc__.strip())
    out, specs = sys.argv[1], sys.argv[2:]
    if len(specs) > MAX_ENTRY:
        die(f"лент {len(specs)} > {MAX_ENTRY} (каталог 0x68-0xFF)")

    entries = [parse_spec(s) for s in specs]

    blob = bytearray(HDR_SIZE)
    blob[0:4] = b"SPA1"
    blob[4] = len(entries)

    off = HDR_SIZE
    for i, e in enumerate(entries):
        d = DIR_OFF + i * 8
        blob[d:d + 2] = off.to_bytes(2, "little")
        blob[d + 2] = e["fw"]
        blob[d + 3] = e["fh"]
        blob[d + 4] = e["nx"]
        blob[d + 5] = e["ny"]
        blob += e["w"].to_bytes(2, "little")   # getimage-заголовок ленты
        blob += e["h"].to_bytes(2, "little")
        blob += e["pix"]
        off = len(blob)

    if len(blob) > PAGE_SIZE:
        die(f"итог {len(blob)} Б > {PAGE_SIZE} (одна EMM-страница); "
            f"разбейте на несколько .atl")

    with open(out, "wb") as f:
        f.write(blob)
    print(f"mkatlas: {out}: лент {len(entries)}, "
          f"{len(blob)} Б (свободно {PAGE_SIZE - len(blob)})")
    for i, e in enumerate(entries):
        print(f"  [{i}] {os.path.basename(e['path'])}: кадр "
              f"{e['fw']}x{e['fh']}, сетка {e['nx']}x{e['ny']}")


if __name__ == "__main__":
    main()
