#!/usr/bin/env python3
"""
pop_pack_bg.py — упаковщик спрайтов СТАТИЧЕСКОГО ФОНА PoP (Шаг 2 порта).

Что делает:
  1. Определяет РЕАЛЬНО используемый набор спрайтов фона (прогоняет
     render_room.py по всем комнатам всех уровней, собирает (chtab,id)).
  2. Грузит исходные PNG (VDUNGEON→VPALACE каскад, как в игре), переводит
     16-цветный индексированный пиксель в 8bpp getimage-блоб:
        индекс 0  -> 0xFF (прозрачность getimage)
        индекс i  -> 0x50+i (env, chtab_6) / 0x60+i (wall, chtab_7)
  3. Пакует в атласы .atl с ПРЯМОЙ адресацией (idx = id), каталог любого
     размера (блобы кладутся ПОСЛЕ каталога — загрузчик atlas_image читает
     offset явно, лимит 19 из libbgi/mkatlas.py не действует).  libbgi НЕ
     трогаем.  Дырки в каталоге (неиспользуемые id) = фейковые записи,
     ссылающиеся на общий пустой 0×0-блоб (осознанная трата места в EMM
     ради нулевых remap-таблиц в W2 — требование пользователя).
  4. Собирает палитру Sprinter (.pal, записи B,G,R,0) из res200.pal (env
     -> слоты 0x50..0x5F) и res360.pal (wall -> 0x60..0x6F).  VGA 6-бит
     каналы масштабируются в 8-бит.
  5. Пишет C-заголовок с раскладкой (имена файлов, SHIFT, число страниц,
     набор fore-id).

Раскладка (прямая адресация, SHIFT=5):
  ENV фон  : 5 страниц, страница = id>>5, idx = id&31
  WALL     : 1 страница, idx = id
  FORE     : 1 страница (спрайты «перед персонажем»: фронты столбов/
             ворот/дебриса), idx = id — отдельный атлас (жизненный цикл
             другой: рисуются каждый кадр как sprite_t, не запекаются).
W2-цена: 7 хэндлов atlas_t (~21 Б), ноль таблиц id->позиция.

Выход: в applications/PoP/poc/res/bg/ — *.atl + pop_bg.pal + pop_bg_atlas.h
"""
import os
import sys
from PIL import Image

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import render_room as R   # noqa: E402  (экстрактор + каскад загрузки)

OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg")

# Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты
# столбов, ворот, дебриса.  См. tile_table.fore_id + анализ pop_background_strategy.
FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100}

# Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется
# рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ
# попадает в used render_room, добавляем явно в env-фон.  (32 обычно уже есть.)
CLIMB_OVERLAY_ENV_IDS = {32, 150, 151}

# Дрожащие/падающие кадры loose-плиты (loose_fram_left/right/bottom, seg008):
# 41/42/43 = покой (уже в used из render_room), 69-74 = вибрация/раздвоение —
# используются ТОЛЬКО динамикой (shake/fall), в статический render_room НЕ
# попадают, поэтому добавляем явно.  Без них дрожащая плита невидима.
LOOSE_ANIM_ENV_IDS = {69, 70, 71, 72, 73, 74}

# Кадры ВЫДВИЖЕНИЯ пик (seg008): три части — left (128-132, своя ячейка,
# draw_tile_anim), right (134-138, правый сосед, draw_tile_anim_right), fore
# (139-143, поверх Kid, draw_tile_fore).  Рисуются ТОЛЬКО динамикой
# (animate_spike); в уровнях пики статически убраны (modif=0 → кадр 0),
# поэтому в used render_room НЕ попадают.  Без них выдвинутые пики невидимы.
SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132,          # left
                      134, 135, 136, 137, 138,          # right
                      139, 140, 141, 142, 143}          # fore

# Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51,
# seg008 draw_gate): 50-59 + 67.  В уровнях ворота статически ЗАКРЫТЫ
# (низкий modif → используются только нижние срезы 50-53); срезы более
# открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно.
GATE_ANIM_ENV_IDS = {50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 67}

# НАЖАТАЯ кнопка-closer (get_tile_to_draw seg008:2FE: при таймере связи >1
# tiles_6_closer рисуется как tiles_5_stuck).  tile_table[5] = база 41 (уже
# есть), правая грань 35, нижняя 36 — оба id встречаются ТОЛЬКО в этом
# динамическом состоянии, статически тайла 5 в уровнях нет, значит в used
# render_room они не попадают.  Без них у нажатой кнопки чёрный провал на
# месте нижней грани и потерянная правая грань.
STUCK_ENV_IDS = {35, 36}

# Левая половина кнопки-opener, когда СЛЕВА пусто (draw_tile_base seg008:0A8E:
# curr_tile==15 && tile_left==0 && level_type==0 → id 148 вместо базы 147).
# render_room этой ветки не знает, поэтому добавляем явно.
OPENER_NOFLOOR_ENV_IDS = {148}

# ---- chtab_1 (flame/sword/potion), база ресурса 150, папка PRINCE ----
# Зелье (seg008 draw_tile_fore/draw_tile_anim): склянка id 12 (малая) / 13
# (большая, типы 2..4), кадры пузырька 16..22 (potion_fram_bubb) и его маска
# 23.  Пузырёк и маска рисуются MONO-блитом (одним цветом), поэтому пакуем их
# силуэтами уже в нужном цвете: маска — чёрный env[0], пузырёк — env[12]
# (красный, тип «лечение»).  Цвета других типов (9 синий / 10 зелёный) —
# TODO, когда появятся такие зелья.
POT_DIR_NAME = "PRINCE"
POT_FLAME_IDS = tuple(range(1, 10))   # пламя факела: modifier+1 (seg008:560)
POT_FLASK_IDS = (12, 13)
POT_BUBBLE_IDS = (16, 17, 18, 19, 20, 21, 22)
POT_MASK_ID = 23
POT_PAL_BASE = 0x40           # 16 цветов chtab_1 -> слоты 0x40..0x4F
# MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из ОБЩЕЙ
# 16-цветной палитры экрана, а не из палитры chtab.  Кладём её в 0x30..0x3F.
VGA16_PAL_BASE = 0x30
VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(42,21,0),
         (42,42,42),(21,21,21),(21,21,63),(21,63,21),(21,63,63),(63,21,21),
         (63,21,63),(63,63,21),(63,63,63)]     # 6-битные каналы, как в .pal
POT_MASK_COLOR = VGA16_PAL_BASE + 0    # чёрный (blitters_40h_mono без цвета)
POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение»)

# Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле
# рисуется chtab_6 id 146).  В статический render_room не попадает.
TORCH_ENV_IDS = {146}

ENV_SHIFT = 5                 # env фон: страница = id>>5, idx = id&31
ENV_MASK = (1 << ENV_SHIFT) - 1

# ---- .atl формат (== libbgi, но каталог не ограничен 19) --------------
ATL_MAGIC = b"SPA1"
ATL_DIR_OFF = 0x68            # _ATL_DIR_OFF
ATL_MIN_SIZE = 0x100          # atlas_load требует n >= _ATL_DATA_OFF
PAGE_SIZE = 16384


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


# ---- 1. Набор используемых спрайтов ----------------------------------
def collect_used():
    """Прогон render_room по всем комнатам -> множество (chtab, id)."""
    used = set()
    orig = R.load_sprite

    def spy(id, chtab=R.CHTAB_ENV, reason=""):
        img = orig(id, chtab, reason)
        if img is not None and id > 0:
            used.add((chtab, id))
        return img

    R.load_sprite = spy
    for lvl in range(1, 15):
        fn = os.path.join(R.LEVEL_DIR, f"res{2000 + lvl}.bin")
        if not os.path.exists(fn):
            continue
        lv = R.Level(open(fn, "rb").read())
        for room in range(1, 25):
            rm = R.Room(lv, room)
            L = lv.links[room][0]
            if L:
                rm.left_room = R.Room(lv, L)
            canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0))
            for dr in (2, 1, 0):
                for c in range(10):
                    R.draw_tile(canvas, rm, dr, c)
    R.load_sprite = orig
    return used


# ---- 2. Загрузка спрайта в 8bpp-блоб ---------------------------------
# Спрайты chtab_6, которые оригинал рисует БЕЗ прозрачности
# (blitters_0_no_transp): нижние грани тайлов (draw_tile_bottom, seg008:570) и
# нижние кадры loose (draw_loose, seg008:600).  У 96/149 (грани кнопок) и 73/74
# (кадры loose) ПОСЛЕДНЯЯ строка — пиксель 0; в оригинале она заливается ЧЁРНЫМ
# поверх того, что сзади, а у нас 0 -> 0xFF делал её прозрачной, и сквозь неё
# просвечивал Kid (спуск/подъём у кромки кнопки).  43/36 полностью непрозрачны —
# для них флаг ничего не меняет, оставлены для полноты списка.
# Дверь уровня (draw_leveldoor, seg008:1D29): решётка створки (33 — слайс 4px,
# 34 — верх), лестница за ней (99 — низ, 144 — марш).  render_room.py
# draw_leveldoor не реализует, поэтому в автосбор эти id не попадают.
LEVELDOOR_ENV_IDS = {33, 34, 99, 144}

# Куски двери уровня (draw_leveldoor) оригинал рисует blitters_0_no_transp:
# марш лестницы 144 обязан ЗАКРАШИВАТЬ поднявшуюся створку, иначе слева от
# лестницы сквозь него видна решётка (0 -> 0xFF делал фон спрайта прозрачным).
NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149} | LEVELDOOR_ENV_IDS


def load_indexed(chtab, id, opaque=False):
    """PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp).  Каскад VDUNGEON->VPALACE.
    Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).

    opaque=True — пиксель 0 остаётся ЦВЕТОМ (base+0 = чёрный), а не ключом
    прозрачности: порт blitters_0_no_transp (см. NO_TRANSP_ENV_IDS)."""
    base_res = 360 if chtab == R.CHTAB_WALL else 200
    pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50
    fname = f"res{base_res + id}.png"
    path = None
    for d in (R.VDUNGEON_DIR, R.VPALACE_DIR):
        p = os.path.join(d, fname)
        if os.path.exists(p):
            path = p
            break
    if path is None:
        die(f"нет файла {fname}")
    im = Image.open(path)
    if im.mode != "P":
        die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
    src = im.tobytes()
    if opaque:
        out = bytes((pal_base + v) for v in src)
    else:
        out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
    return im.width, im.height, out


def load_potion(id, mono=None, opaque=False):
    """chtab_1: res(150+id).png из data/PRINCE.  mono=None — обычный спрайт
    (палитра chtab_1 -> слоты POT_PAL_BASE+i); mono=цвет — силуэт этим цветом
    (порт blitters_40h_mono); opaque=True — пиксель 0 остаётся ЦВЕТОМ
    (порт blitters_0_no_transp, см. POT_FLAME_IDS)."""
    path = os.path.join(os.path.dirname(R.VDUNGEON_DIR), POT_DIR_NAME,
                        f"res{150 + id}.png")
    if not os.path.exists(path):
        die(f"нет файла {path}")
    im = Image.open(path)
    if im.mode != "P":
        die(f"res{150+id}: mode {im.mode}, ожидался P")
    src = im.tobytes()
    if mono is not None:
        out = bytes((0xFF if v == 0 else mono) for v in src)
    elif opaque:
        out = bytes((POT_PAL_BASE + v) for v in src)
    else:
        out = bytes((0xFF if v == 0 else POT_PAL_BASE + v) for v in src)
    return im.width, im.height, out


def blob(w, h, pix):
    """getimage-блоб: u16 w, u16 h (LE) + пиксели."""
    return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix


# ---- 3. Пакер .atl (каталог любого размера) --------------------------
def pack_atlas(path, entries):
    """entries: dict idx -> (w, h, pix).  Пустые idx (0..maxidx) заполняются
    фейковой ссылкой на общий 0×0-блоб.  Каталог count записей с 0x68,
    блобы — сразу после каталога."""
    if not entries:
        die(f"{path}: пустой атлас")
    count = max(entries) + 1
    if count > 255:
        die(f"{path}: count {count} > 255")
    data_off = ATL_DIR_OFF + count * 8

    blobs = bytearray()
    empty_off = data_off + len(blobs)         # общий 0×0-блоб для дырок
    blobs += blob(0, 0, b"")
    dir_bytes = bytearray(count * 8)
    for idx in range(count):
        e = entries.get(idx)
        if e is None:
            off = empty_off
            fw = fh = 0
        else:
            w, h, pix = e
            off = data_off + len(blobs)
            blobs += blob(w, h, pix)
            fw = min(w, 255)
            fh = min(h, 255)
        d = idx * 8
        dir_bytes[d:d + 2] = off.to_bytes(2, "little")
        dir_bytes[d + 2] = fw
        dir_bytes[d + 3] = fh
        dir_bytes[d + 4] = 1                  # nx
        dir_bytes[d + 5] = 1                  # ny
        # d+6,d+7 резерв

    out = bytearray(data_off)
    out[0:4] = ATL_MAGIC
    out[4] = count
    out[ATL_DIR_OFF:ATL_DIR_OFF + len(dir_bytes)] = dir_bytes
    out += blobs
    if len(out) < ATL_MIN_SIZE:
        out += bytes(ATL_MIN_SIZE - len(out))   # atlas_load: n >= 0x100
    if len(out) > PAGE_SIZE:
        die(f"{path}: {len(out)} Б > {PAGE_SIZE} (одна EMM-страница)")
    with open(path, "wb") as f:
        f.write(out)
    return count, len(out)


# ---- 4. Палитра ------------------------------------------------------
def scale6to8(v):
    return (v * 255) // 63


def build_palette(out_path):
    """256-записная палитра B,G,R,0: env@0x50..0x5F, wall@0x60..0x6F."""
    def read_pal(res, d):
        b = open(os.path.join(d, res), "rb").read()
        return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]

    env = read_pal("res200.pal", R.VDUNGEON_DIR)
    wall = read_pal("res360.pal", R.VDUNGEON_DIR)
    pot = read_pal("res150.pal", os.path.join(os.path.dirname(R.VDUNGEON_DIR),
                                              POT_DIR_NAME))
    pal = bytearray(256 * 4)
    for i, (r, g, b) in enumerate(env):
        o = (0x50 + i) * 4
        pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
    for i, (r, g, b) in enumerate(wall):
        o = (0x60 + i) * 4
        pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
    for i, (r, g, b) in enumerate(pot):
        o = (POT_PAL_BASE + i) * 4
        pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
    for i, (r, g, b) in enumerate(VGA16):
        o = (VGA16_PAL_BASE + i) * 4
        pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
    with open(out_path, "wb") as f:
        f.write(pal)


# ---- 5. C-заголовок --------------------------------------------------
def write_header(out_path, env_pages, wall_name, fore_name):
    lines = [
        "/* pop_bg_atlas.h — раскладка атласов статического фона PoP.",
        " * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.",
        " *",
        " * Прямая адресация (ноль remap-таблиц в W2):",
        " *   ENV фон id N  -> atlas env_bg[N>>%d], idx N&%d" % (ENV_SHIFT, ENV_MASK),
        " *   WALL   id N  -> atlas wall,          idx N",
        " *   FORE   id N  -> atlas fore,          idx N",
        " */",
        "#ifndef POP_BG_ATLAS_H",
        "#define POP_BG_ATLAS_H",
        "",
        "#define POP_ENV_SHIFT   %d" % ENV_SHIFT,
        "#define POP_ENV_MASK    %d" % ENV_MASK,
        "#define POP_ENV_PAGES   %d" % len(env_pages),
        "",
        "/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */",
        "#define POP_PAL_ENV     0x50",
        "#define POP_PAL_WALL    0x60",
        "",
        "/* Имена файлов атласов (грузятся atlas_load). */",
        "static const char *const pop_env_atl[POP_ENV_PAGES] = {",
    ]
    for pg in range(len(env_pages)):
        nm = env_pages[pg]
        lines.append('    %s,' % ('"%s"' % nm if nm else "0 /* пусто */"))
    lines += [
        "};",
        '#define POP_WALL_ATL    "%s"' % wall_name,
        '#define POP_FORE_ATL    "%s"' % fore_name,
        '#define POP_POT_ATL     "pop_pot.atl"   /* chtab_1: зелья */',
        "#define POP_PAL_POT     0x%02X" % POT_PAL_BASE,
        '#define POP_BG_PAL      "pop_bg.pal"',
        "",
        "#endif",
        "",
    ]
    with open(out_path, "w") as f:
        f.write("\n".join(lines))


def main():
    os.makedirs(OUT_DIR, exist_ok=True)
    used = collect_used()
    env_ids = sorted(set(id for ch, id in used if ch != R.CHTAB_WALL)
                     | CLIMB_OVERLAY_ENV_IDS           # + пол-оверлей подъёма
                     | LOOSE_ANIM_ENV_IDS              # + дрожащие кадры loose
                     | SPIKE_ANIM_ENV_IDS              # + кадры выдвижения пик
                     | GATE_ANIM_ENV_IDS               # + кадры открытия ворот
                     | TORCH_ENV_IDS                   # + основание факела
                     | STUCK_ENV_IDS                   # + нажатая кнопка-closer
                     | OPENER_NOFLOOR_ENV_IDS          # + opener без пола слева
                     | LEVELDOOR_ENV_IDS)              # + дверь уровня (створка/лестница)
    wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)

    env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]
    env_fore = [i for i in env_ids if i in FORE_ENV_IDS]

    # ENV фон -> страницы по id>>SHIFT
    pages = {}
    for id in env_bg:
        w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS)
        pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
    max_pg = max(pages) if pages else -1
    env_page_names = []
    total_bytes = 0
    for pg in range(max_pg + 1):
        if pg in pages:
            nm = f"pop_env{pg}.atl"
            cnt, sz = pack_atlas(os.path.join(OUT_DIR, nm), pages[pg])
            total_bytes += sz
            env_page_names.append(nm)
            print(f"  {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б")
        else:
            env_page_names.append(None)
            print(f"  (env стр {pg}: пусто)")

    # WALL -> idx=id
    wall_entries = {id: load_indexed(R.CHTAB_WALL, id) for id in wall_ids}
    cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_wall.atl"), wall_entries)
    total_bytes += sz
    print(f"  pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")

    # FORE -> idx=id
    fore_entries = {id: load_indexed(R.CHTAB_ENV, id) for id in env_fore}
    cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries)
    total_bytes += sz
    print(f"  pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")

    # POTION (chtab_1) -> idx=id: склянки + кадры пузырька + маска
    # chtab_1 целиком (res151..res173 = id 1..23): пламя факелов, меч-предмет,
    # склянки зелий.  Пузырьки и их маска рисуются MONO-блитом — пакуем
    # силуэтами в нужном цвете (см. POT_*_COLOR).
    pot_entries = {}
    for id in range(1, 24):
        if id in POT_BUBBLE_IDS:
            pot_entries[id] = load_potion(id, POT_BUBBLE_COLOR)
        elif id == POT_MASK_ID:
            pot_entries[id] = load_potion(id, POT_MASK_COLOR)
        elif id in POT_FLAME_IDS:
            # Пламя оригинал рисует blitters_0_no_transp (seg008
            # draw_tile_anim_right) — непрозрачным боксом поверх всего, что
            # сзади.  Все 9 кадров лежат на одном канвасе 16x18, поэтому
            # непрозрачный блит сам стирает предыдущий кадр: heal не нужен.
            pot_entries[id] = load_potion(id, opaque=True)
        else:
            pot_entries[id] = load_potion(id)
    cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
    total_bytes += sz
    print(f"  pop_pot.atl (chtab_1: факелы+зелья): {len(pot_entries)} спрайтов, каталог {cnt}, {sz} Б")

    build_palette(os.path.join(OUT_DIR, "pop_bg.pal"))
    write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"),
                 env_page_names, "pop_wall.atl", "pop_fore.atl")

    n_pages = sum(1 for n in env_page_names if n) + 2
    print(f"ИТОГО: {len(env_bg)} env-фон + {len(wall_ids)} wall + "
          f"{len(env_fore)} fore = {len(env_bg)+len(wall_ids)+len(env_fore)} спрайтов")
    print(f"       {n_pages} EMM-страниц, {total_bytes} Б на диске "
          f"(+1КБ палитра), W2: {n_pages}×atlas_t = {n_pages*3} Б хэндлов")
    print(f"       -> {OUT_DIR}")


if __name__ == "__main__":
    main()
