#!/usr/bin/env python3
"""
Parse SDLPoP level file (res2001.bin) and render the first room by
faithfully replaying the tile-drawing logic of seg008.c (draw_room/draw_tile).

Pixel model (from seg008.c):
  - screen is 320px wide, 10 columns.
  - col_xh[col] = {0,4,8,12,16,20,24,28,32,36}; x_px = xh*8  -> 0,32,64,...,288
    (tile column is 32px; the 4px offset handles overlap of floor/wall edges)
  - rows drawn 2,1,0 (bottom..top). For each row:
        draw_bottom_y = 63*drawn_row + 65
        draw_main_y   = draw_bottom_y - 3
    row2 -> bottom_y=191, row1 -> 128, row0 -> 65
  - a tile sprite of height h drawn at ybottom is placed with its BOTTOM at ybottom:
        y = ybottom - h + 1

We replay draw_tile() which composites (in order):
  floorright, anim_topright, right, anim_right, bottom, loose, base, anim, fore
Dependencies on NEIGHBOURS:
  - tile_left  = tile in the column to the left (same room, same row)
  - row_below_left_[col] = tile in (room_below, col-1, row_below)  -> for topright/floorright
  - For a single room with no links we treat out-of-range neighbours as walls
    (level edges), matching SDLPoP's custom->drawn_tile_*_level_edge defaults
    (left = wall, top = floor).

We ignore animated variants (spike frames, chomper, torch flames, gate
animation) for this first static render; they use curr_modifier/backtable.
"""

import os
from PIL import Image

LEVEL_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/LEVELS"
VPALACE_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VPALACE"
VDUNGEON_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VDUNGEON"

# For dungeon levels (tbl_level_type == 0) the environment sprites come from
# VDUNGEON where present, otherwise fall back to the shared VPALACE set
# (PRINCE.DAT common images). For palace levels use VPALACE only.
IS_DUNGEON = True  # room 1 (res2001) is the dungeon

# tile_table[code] = (base_id, floor_left, base_y, right_id, floor_right,
#                     right_y, stripe_id, topright_id, bottom_id, fore_id,
#                     fore_x, fore_y)
TILE_TABLE = [
    (  0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0), # 0x00 empty
    ( 41,   1,   0,  42,   1,   2, 145,   0,  43,   0,   0,   0), # 0x01 floor
    (127,   1,   0, 133,   1,   2, 145,   0,  43,   0,   0,   0), # 0x02 spike
    ( 92,   1,   0,  93,   1,   2,   0,  94,  43,  95,   1,   0), # 0x03 pillar
    ( 46,   1,   0,  47,   1,   2,   0,  48,  43,  49,   3,   0), # 0x04 gate
    ( 41,   1,   1,  35,   1,   3, 145,   0,  36,   0,   0,   0), # 0x05 stuck floor
    ( 41,   1,   0,  42,   1,   2, 145,   0,  96,   0,   0,   0), # 0x06 close button
    ( 46,   1,   0,   0,   0,   2,   0,   0,  43,  49,   3,   0), # 0x07 door top with floor
    ( 86,   1,   0,  87,   1,   2,   0,   0,  43,  88,   1,   0), # 0x08 big pillar bottom
    (  0,   0,   0,  89,   0,   3,   0,  90,   0,  91,   1,   3), # 0x09 big pillar top
    ( 41,   1,   0,  42,   1,   2, 145,   0,  43,  12,   2,  -3), # 0x0A potion
    (  0,   1,   0,   0,   0,   0, 145,   0,   0,   0,   0,   0), # 0x0B loose floor
    (  0,   0,   0,   0,   0,   2,   0,   0,  85,  49,   3,   0), # 0x0C door top
    ( 75,   1,   0,  42,   1,   2,   0,   0,  43,  77,   0,   0), # 0x0D mirror
    ( 97,   1,   0,  98,   1,   2, 145,   0,  43, 100,   0,   0), # 0x0E debris
    (147,   1,   0,  42,   1,   1, 145,   0, 149,   0,   0,   0), # 0x0F open button
    ( 41,   1,   0,  37,   0,   0,   0,  38,  43,   0,   0,   0), # 0x10 leveldoor left
    (  0,   0,   0,  39,   1,   2,   0,  40,  43,   0,   0,   0), # 0x11 leveldoor right
    (  0,   0,   0,  42,   1,   2, 145,   0,  43,   0,   0,   0), # 0x12 chomper
    ( 41,   1,   0,  42,   1,   2,   0,   0,  43,   0,   0,   0), # 0x13 torch
    (  0,   0,   0,   1,   1,   2,   0,   2,   0,   0,   0,   0), # 0x14 wall
    ( 30,   1,   0,  31,   1,   2,   0,   0,  43,   0,   0,   0), # 0x15 skeleton
    ( 41,   1,   0,  42,   1,   2, 145,   0,  43,   0,   0,   0), # 0x16 sword
    ( 41,   1,   0,  10,   0,   0,   0,  11,  43,   0,   0,   0), # 0x17 balcony left
    (  0,   0,   0,  12,   1,   2,   0,  13,  43,   0,   0,   0), # 0x18 balcony right
    ( 92,   1,   0,  42,   1,   2, 145,   0,  43,  95,   1,   0), # 0x19 lattice pillar
    (  1,   0,   0,   0,   0,   0,   0,   0,   2,   9,   0, -53), # 0x1A lattice down
    (  3,   0, -10,   0,   0,   0,   0,   0,   0,   9,   0, -53), # 0x1B lattice small
    (  4,   0, -10,   0,   0,   0,   0,   0,   0,   9,   0, -53), # 0x1C lattice left
    (  5,   0, -10,   0,   0,   0,   0,   0,   0,   9,   0, -53), # 0x1D lattice right
    ( 97,   1,   0,  98,   1,   2,   0,   0,  43, 100,   0,   0), # 0x1E debris with torch
]

TILE_NAMES = {
    0x00: "empty", 0x01: "floor", 0x02: "spike", 0x03: "pillar",
    0x04: "gate", 0x05: "stuck_floor", 0x06: "close_button", 0x07: "door_top_floor",
    0x08: "big_pillar_bottom", 0x09: "big_pillar_top", 0x0A: "potion",
    0x0B: "loose_floor", 0x0C: "door_top", 0x0D: "mirror", 0x0E: "debris",
    0x0F: "open_button", 0x10: "leveldoor_left", 0x11: "leveldoor_right",
    0x12: "chomper", 0x13: "torch", 0x14: "wall", 0x15: "skeleton",
    0x16: "sword", 0x17: "balcony_left", 0x18: "balcony_right",
    0x19: "lattice_pillar", 0x1A: "lattice_down", 0x1B: "lattice_small",
    0x1C: "lattice_left", 0x1D: "lattice_right", 0x1E: "debris_torch",
}

COL_XH = [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]

# chtab -> directory/subfolder + id offset
# id_chtab_6_environment  -> VPALACE, res(200+id).png
# id_chtab_7_environmentwall -> VPALACE (wall images also here, res(200+id))
# id_chtab_1_flameswordpotion -> flames etc (skipped in static render)
CHTAB_ENV = 6
CHTAB_WALL = 7
CHTAB_FSP = 1   # flames/sword/potion (animated, skipped)


# ---- POP1 level file structure (see POP-DAT-FormatSpecifications.txt, sec 3.4) ----
# Block layout (for the standard 2305-byte level):
#   0     .. 720   pop1 foretable  (24 rooms x 30 bytes)
#   720   .. 1440  pop1 backtable  (24 rooms x 30 bytes)
#   1440  .. 1696  door I         (256 bytes, one per event line)
#   1696  .. 1952  door II        (256 bytes)
#   1952  .. 2048  links          (24 rooms x 4 bytes: L,R,Up,Dn)
#   2048  .. 2112  unknown I
#   2112  .. 2115  start position (room, location, direction)
#   2115  .. 2116  unknown II
#   2116  .. 2119  unknown III
#   2119  .. 2143  guard location (24)
#   2143  .. 2167  guard direction (24)
#   2167  .. 2191  unknown IV (a)
#   2191  .. 2215  unknown IV (b)
#   2215  .. 2239  guard skill (24)
#   2239  .. 2263  unknown IV (c)
#   2263  .. 2287  guard colour (24)
#   2287  .. 2303  unknown IV (d)
#   2303  .. 2305  0F 09

# foretable byte format: rrmccccc  (2 random bits, 1 modifier bit, 5 code bits)
FG_RAND_MASK = 0xC0
FG_MODBIT_MASK = 0x20
FG_CODE_MASK = 0x1F

# backtable: a tile modifier that depends on the group of the foretable code.
# group ids map foretable code -> "group" used to interpret backtable value.
# (see Table 7/8).  We only need the group for door/event interpretation.
GROUP_OF_CODE = {
    0x00: "free", 0x01: "free", 0x02: "spike", 0x03: "none",
    0x04: "gate", 0x05: "none", 0x06: "event", 0x07: "tapest",
    0x08: "none", 0x09: "none", 0x0A: "potion", 0x0B: "none",
    0x0C: "ttop", 0x0D: "none", 0x0E: "none", 0x0F: "event",
    0x10: "none", 0x11: "none", 0x12: "chomp", 0x13: "none",
    0x14: "wall", 0x15: "none", 0x16: "none", 0x17: "none",
    0x18: "none", 0x19: "none", 0x1A: "none", 0x1B: "none",
    0x1C: "none", 0x1D: "none", 0x1E: "none", 0x1F: "none",
}

# backtable modifier meanings per group (subset, from Table 8):
GATE_MOD = {0: "closed", 1: "open", 2: "with_lattice", 3: "alt_design"}
TTOP_MOD = {0: "normal", 1: "black", 2: "empty", 3: "?3", 4: "?4", 5: "?5", 6: "?6", 7: "?7"}
SPIKE_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out"}
CHOMP_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out", 4: "out?"}
POTION_MOD = {0: "blue", 1: "no_blue", 2: "window", 3: "spot?"}
TAPEST_MOD = {0: "normal", 1: "alt_design", 2: "black", 3: "empty"}
EVENT_MOD_DESC = {  # group "event": the backtable byte IS the event id
    0: "closer", 1: "opener",
}


class Level:
    """Full parsed POP1 level file."""
    def __init__(self, data):
        self.data = data
        self.rooms_fg = {}   # room -> list[30] of (fg_byte, code, modbit)
        self.rooms_bg = {}   # room -> list[30] of backtable byte
        for room in range(1, 25):
            fg = []; bg = []
            for off in range(30):
                i = (room - 1) * 30 + off
                fb = data[i]; bb = data[720 + i]
                fg.append((fb, fb & FG_CODE_MASK, (fb & FG_MODBIT_MASK) >> 5))
                bg.append(bb)
            self.rooms_fg[room] = fg
            self.rooms_bg[room] = bg
        # door I / II (event lines 0..255)
        self.door_i = data[1440:1696]
        self.door_ii = data[1696:1952]
        # links: 24 rooms x (L,R,Up,Dn)
        self.links = {}
        for room in range(1, 25):
            b = data[1952 + (room - 1) * 4: 1952 + (room - 1) * 4 + 4]
            self.links[room] = tuple(b)  # (left, right, up, down)
        # start position
        self.start = tuple(data[2112:2115])
        # guards
        self.guard_loc = list(data[2119:2143])
        self.guard_dir = list(data[2143:2167])
        self.guard_skill = list(data[2215:2239])
        self.guard_colour = list(data[2263:2287])

    def door_for_event(self, event_id):
        """Decode door I/II for an event line -> (room, location, trigger_next)."""
        bi = self.door_i[event_id]
        bii = self.door_ii[event_id]
        # Byte I: t1 s4 s5 l1 l2 l3 l4 l5
        t1 = (bi >> 7) & 1
        s4 = (bi >> 6) & 1
        s5 = (bi >> 5) & 1
        l1 = (bi >> 4) & 1
        l2 = (bi >> 3) & 1
        l3 = (bi >> 2) & 1
        l4 = (bi >> 1) & 1
        l5 = bi & 1
        # Byte II: s1 s2 s3 00000
        s1 = (bii >> 7) & 1
        s2 = (bii >> 6) & 1
        s3 = (bii >> 5) & 1
        screen = (s1 << 4) | (s2 << 3) | (s3 << 2) | (s4 << 1) | s5
        loc = (l1 << 4) | (l2 << 3) | (l3 << 2) | (l4 << 1) | l5
        return screen, loc, t1

    def backtable_desc(self, code, bb):
        grp = GROUP_OF_CODE.get(code, "none")
        tbl = {"gate": GATE_MOD, "ttop": TTOP_MOD, "spike": SPIKE_MOD,
               "chomp": CHOMP_MOD, "potion": POTION_MOD, "tapest": TAPEST_MOD}
        if code == 0x06 or code == 0x0F:  # event group
            act = EVENT_MOD_DESC.get(bb & 1, "?")
            return f"event(id={bb}, {act})"
        if grp in tbl:
            return tbl[grp].get(bb, f"?0x{bb:02X}")
        return f"0x{bb:02X}"


class Room:
    def __init__(self, level, room_num):
        self.level = level
        self.room_num = room_num
        self.fg = level.rooms_fg[room_num]
        self.bg = level.rooms_bg[room_num]
        self.left_room = None   # соседняя комната слева (link L); load_leftroom

    def tile(self, row, col):
        """Return (code, fg_byte, modbit, backtable_byte) for an in-room tile.
        Out-of-range neighbours follow seg008 defaults:
          col<0 -> берётся из ЛЕВОЙ комнаты (col 9), как load_leftroom();
                   если левой комнаты нет — уровневая кромка = стена (20).
          row<0 -> top level edge = floor (code 1, mod 0)
        """
        if col < 0:
            if self.left_room is not None and 0 <= row < 3:
                return self.left_room.tile(row, 9)
            return 20, 0, 0, 0
        if row < 0:
            return 1, 0, 0, 0
        if not (0 <= row < 3 and 0 <= col < 10):
            return 20, 0, 0, 0
        i = row * 10 + col
        fb, code, mbit = self.fg[i]
        return code, fb, mbit, self.bg[i]


# Log of every sprite loaded during a render, for the per-tile report.
SPRITE_LOG = []


def load_sprite(id, chtab=CHTAB_ENV, reason=""):
    """Load resource PNG для картинки id из набора chtab.

    База ресурса зависит от chtab (seg000.c:1109/1122):
      chtab_6 environment      -> res(200+id)
      chtab_7 environmentwall  -> res(360+id)
      chtab_1 flame/sword/pot  -> res(150+id)  (анимация, статикой не грузим)

    Dungeon (IS_DUNGEON) cascade: try VDUNGEON first (dungeon-specific
    overrides), then fall back to VPALACE (shared base set).
    """
    if id <= 0:
        return None
    base = {CHTAB_ENV: 200, CHTAB_WALL: 360, CHTAB_FSP: 150}.get(chtab, 200)
    fname = f"res{base + id}.png"
    if IS_DUNGEON:
        vd = os.path.join(VDUNGEON_DIR, fname)
        if os.path.exists(vd):
            try:
                img = Image.open(vd).convert("RGBA")
                CURRENT_TILE_LOG.append((id, "VDUNGEON", reason))
                return img
            except Exception:
                pass
    vp = os.path.join(VPALACE_DIR, fname)
    try:
        img = Image.open(vp).convert("RGBA")
        CURRENT_TILE_LOG.append((id, "VPALACE", reason))
        return img
    except FileNotFoundError:
        CURRENT_TILE_LOG.append((id, "MISSING", reason))
        return None


def blit(canvas, img, x, y):
    """Composite img onto canvas at (x,y) using its alpha as mask (draw_main style)."""
    if img is None:
        return
    w, h = img.size
    if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height:
        return
    canvas.alpha_composite(img, (x, y))


def blit_black(canvas, img, x, y):
    """blitters_9_black (seg008.c:1054): рисует МАСКУ спрайта ЧЁРНЫМ —
    непрозрачная область формы заливается чёрным (тень-силуэт), а не
    пикселями картинки.  Используется для floor-right в draw_tile_floorright
    (тень уступа тайла снизу-слева)."""
    if img is None:
        return
    w, h = img.size
    if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height:
        return
    alpha = img.split()[3]
    solid = Image.new("RGBA", img.size, (0, 0, 0, 255))
    canvas.alpha_composite(Image.composite(solid, Image.new("RGBA", img.size, (0, 0, 0, 0)), alpha), (x, y))


# ---- Dungeon wall_pattern() (seg008.c:1928) ----
# PRNG: seg009.c:321  random_seed = random_seed*214013 + 2531011; return (random_seed>>16) % (max+1)
_random_seed = 0


def prandom(maxval):
    global _random_seed
    _random_seed = (_random_seed * 214013 + 2531011) & 0xFFFFFFFF
    return (_random_seed >> 16) % (maxval + 1)


# wall sprite ids (chtab_7 / RSET_WALL = 7)  res(200+id).png
RES_WALL_FACE_MAIN, RES_WALL_FACE_TOP, RES_WALL_CENTRE_BASE, RES_WALL_CENTRE_MAIN = 1, 2, 3, 4
RES_WALL_RIGHT_BASE, RES_WALL_RIGHT_MAIN, RES_WALL_SINGLE_BASE, RES_WALL_SINGLE_MAIN = 5, 6, 7, 8
RES_WALL_LEFT_BASE, RES_WALL_LEFT_MAIN, RES_WALL_DIVIDER1, RES_WALL_DIVIDER2 = 9, 10, 11, 12
RES_WALL_RNDBLOCK = 13
RES_WALL_MARK_TL, RES_WALL_MARK_BL, RES_WALL_MARK_TR, RES_WALL_MARK_BR = 14, 15, 16, 17

WALL_MODIFIER_SWS, WALL_MODIFIER_SWW, WALL_MODIFIER_WWS, WALL_MODIFIER_WWW = 0, 1, 2, 3

# Сплошная грань стены (chtab_7), индекс = wall_modifier (seg008.c:567/687):
WALL_FRAM_BOTTOM = [7, 9, 5, 3]   # нижняя часть (draw_tile_bottom)
WALL_FRAM_MAIN   = [8, 10, 6, 4]  # основная часть (draw_tile_fore)

LPOS = [58, 41, 37, 20, 16]   # draw_left_mark vertical offsets
RPOS = [52, 42, 31, 21]       # draw_right_mark vertical offsets

TBL_LINE = [0, 10, 20]        # смещение строки в 30-тайловом массиве комнаты


def wall_modifier(room, drawn_row, col):
    """Модификатор стены (SWS/SWW/WWS/WWW = 0..3) по соседям слева/справа.
    seg008.c:1255-1305 (без USE_FAKE_TILES): сосед считается стеной, если
    его тип == wall(20).  На кромке комнаты без линка сосед по умолчанию
    считается стеной (wall_to_left/right = 1)."""
    left_code = room.tile(drawn_row, col - 1)[0] if col > 0 else 20
    right_code = room.tile(drawn_row, col + 1)[0] if col < 9 else 20
    wall_left = (left_code == 20)
    wall_right = (right_code == 20)
    if wall_left and wall_right:
        return WALL_MODIFIER_WWW   # 3
    if wall_left:
        return WALL_MODIFIER_WWS   # 2
    if wall_right:
        return WALL_MODIFIER_SWW   # 1
    return WALL_MODIFIER_SWS       # 0


def _wall_add(canvas, which_table, id, xh, xl, ybottom, reason):
    """Emulate ptr_add_table(RSET_WALL, id, xh, xl, ybottom, blit, 0).
    which_table 0 -> backtable (blit transparent), 1 -> foretable.
    RSET_WALL id -> res(200+id).png, cascade VDUNGEON->VPALACE."""
    img = load_sprite(id, CHTAB_WALL, reason=reason)
    if img is None:
        return
    y = ybottom - img.height + 1
    x = xh * 8 + xl
    blit(canvas, img, x, y)


def draw_left_mark(canvas, decal_variant, arg2, arg1, drawn_row, col):
    image_id = RES_WALL_MARK_TL
    if decal_variant % 2:
        image_id = RES_WALL_MARK_BL
    lv2 = 0
    if decal_variant > 3:
        lv2 = arg1 + 6
    elif decal_variant > 1:
        lv2 = arg2 + 6
    xh = COL_XH[col]
    draw_bottom_y = 63 * drawn_row + 65
    _wall_add(canvas, 1, image_id, xh + (1 if decal_variant in (2, 3) else 0),
              lv2, draw_bottom_y - LPOS[decal_variant],
              reason=f"wall left-mark var{decal_variant}")


def draw_right_mark(canvas, decal_variant, arg1, drawn_row, col):
    image_id = RES_WALL_MARK_TR
    if decal_variant % 2:
        image_id = RES_WALL_MARK_BR
    a = 24 if decal_variant < 2 else arg1 - 3
    xh = COL_XH[col]
    draw_bottom_y = 63 * drawn_row + 65
    _wall_add(canvas, 1, image_id, xh + (1 if decal_variant > 1 else 0),
              a, draw_bottom_y - RPOS[decal_variant],
              reason=f"wall right-mark var{decal_variant}")


def wall_pattern(canvas, room, drawn_row, col, which_part, which_table):
    """Replay seg008.c wall_pattern() for a dungeon wall tile.
    which_part: 0 = bottom part (draw_tile_bottom), 1 = fore part (draw_tile_fore).
    which_table: 0 = backtable, 1 = foretable."""
    global _random_seed
    xh = COL_XH[col]
    draw_bottom_y = 63 * drawn_row + 65
    draw_main_y = draw_bottom_y - 3

    saved_seed = _random_seed
    # seg008.c:1941 — зерно зависит от НОМЕРА КОМНАТЫ (не строки!):
    #   random_seed = drawn_room + tbl_line[drawn_row] + drawn_col
    room_num = getattr(room, "room_num", 1)
    _random_seed = room_num + TBL_LINE[drawn_row] + col
    prandom(1)  # discard
    is_dungeon = IS_DUNGEON

    middle_divider = prandom(1)
    middle_divider_offset = prandom(4)
    bottom_divider = prandom(1)
    bottom_divider_offset = prandom(4)

    # Модификатор стены (какие соседи — стены) — тот же, что для сплошной грани.
    bg = wall_modifier(room, drawn_row, col)

    if bg == WALL_MODIFIER_WWW:
        if which_part != 0:
            if prandom(4) == 0:
                _wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0,
                          draw_bottom_y - 42, reason="wall rndblock")
            _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1,
                      middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider")
        _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh,
                  bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider")
        if which_part != 0 and is_dungeon:
            if prandom(4) == 0:
                draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col)
            if prandom(4) == 0:
                draw_left_mark(canvas, prandom(4),
                               middle_divider_offset - middle_divider,
                               bottom_divider_offset - bottom_divider, drawn_row, col)
    elif bg == WALL_MODIFIER_SWS:
        if is_dungeon and which_part != 0:
            if prandom(6) == 0:
                draw_left_mark(canvas, prandom(1),
                               middle_divider_offset - middle_divider,
                               bottom_divider_offset - bottom_divider, drawn_row, col)
    elif bg == WALL_MODIFIER_SWW:
        if which_part != 0:
            if prandom(4) == 0:
                _wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0,
                          draw_bottom_y - 42, reason="wall rndblock")
            _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1,
                      middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider")
            if is_dungeon:
                if prandom(4) == 0:
                    draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col)
                if prandom(4) == 0:
                    draw_left_mark(canvas, prandom(3),
                                   middle_divider_offset - middle_divider,
                                   bottom_divider_offset - bottom_divider, drawn_row, col)
    elif bg == WALL_MODIFIER_WWS:
        if which_part != 0:
            _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1,
                      middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider")
        _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh,
                  bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider")
        if which_part != 0 and is_dungeon:
            if prandom(4) == 0:
                draw_right_mark(canvas, prandom(1) + 2, middle_divider_offset, drawn_row, col)
            if prandom(4) == 0:
                draw_left_mark(canvas, prandom(4),
                               middle_divider_offset - middle_divider,
                               bottom_divider_offset - bottom_divider, drawn_row, col)

    _random_seed = saved_seed


# Per-tile sprite log: draw_tile resets CURRENT_TILE_LOG, load_sprite appends to it.
CURRENT_TILE_LOG = []
TILE_LOGS = {}  # (row, col) -> list of (id, source, reason)

# seg008.c:1108 — вертикальные слайсы решётки ворот (portcullis).
DOOR_FRAM_SLICE = [67, 59, 58, 57, 56, 55, 54, 53, 52]
# seg008.c:517 — правый край шипов по кадру get_spike_frame.
SPIKES_FRAM_RIGHT = [0, 134, 135, 136, 137, 138, 137, 135, 134, 0]
# seg008.c:450 — «blueline»-декали на ЗАДНЕЙ стене (силуэты кладки/окна),
# рисуются правым соседом по МОДИФИКАТОРУ соседа слева:
#   tile_left == empty -> blueline_fram1[mod] (окно/решётка-фрагменты 124..126)
#                         на высоте blueline_fram_y[mod] + main_y;
#   tile_left == floor -> blueline_fram3[mod] (кирпичные силуэты 44/45)
#                         на main_y - 20 (если mod != !!level_type).
BLUELINE_FRAM1 = [0, 124, 125, 126]
BLUELINE_FRAM_Y = [0, -20, -20, 0]
BLUELINE_FRAM3 = [44, 44, 45, 45]


def draw_gate_back(canvas, modifier_left, draw_xh, draw_bottom_y, draw_main_y):
    """seg008.c:1110 draw_gate_back — портикулис (решётка) ворот.  Рисуется
    на тайле СПРАВА от ворот (draw_tile_anim_right, tile_left==gate).  У нас
    это col 0 room1, где сосед слева — ворота из room 5.  modifier_left —
    модификатор тайла-ворот (степень поднятия решётки)."""
    x = draw_xh * 8
    gate_top_y = draw_bottom_y - 62
    gate_openness = (min(modifier_left, 188) >> 2) + 1
    gate_bottom_y = draw_main_y - gate_openness
    if gate_bottom_y + 12 < draw_main_y:
        img = load_sprite(50, reason="gate bottom+B")   # gate поднята высоко
        if img:
            blit(canvas, img, x, gate_bottom_y - img.height + 1)
    else:
        # id 47 (правая грань ворот) уже рисует draw_tile_right — не дублируем.
        img = load_sprite(51, reason="gate bottom")
        if img:
            blit(canvas, img, x, (gate_bottom_y - 2) - img.height + 1)
    # Стек 8px-слайсов решётки снизу вверх.
    ybottom = gate_bottom_y - 12
    if ybottom < 192:
        while ybottom >= 0 and ybottom > 7 and (ybottom - 7) > gate_top_y:
            img = load_sprite(52, reason="gate slice 8px")
            if img:
                blit(canvas, img, x, ybottom - img.height + 1)
            ybottom -= 8
    gate_frame = ybottom - gate_top_y + 1
    if 0 < gate_frame < 9:
        img = load_sprite(DOOR_FRAM_SLICE[gate_frame], reason="gate top slice")
        if img:
            blit(canvas, img, x, ybottom - img.height + 1)


def draw_tile(canvas, room, drawn_row, col, left_edge_code=20):
    global CURRENT_TILE_LOG
    CURRENT_TILE_LOG = []
    code, fg_byte, modbit, bg_mod = room.tile(drawn_row, col)
    # tile_left: колонка слева.  col-1 == -1 -> Room.tile берёт из левой
    # комнаты (или уровневая кромка = стена), см. Room.tile / load_leftroom.
    left_code, _, _, left_mod = room.tile(drawn_row, col - 1)
    draw_xh = COL_XH[col]
    x = draw_xh * 8
    draw_bottom_y = 63 * drawn_row + 65
    draw_main_y = draw_bottom_y - 3

    # For topright/floorright we need the tile below-left.
    # Room below = same room (no room links) -> row_below = drawn_row+1 if <3 else treat as empty/floor top edge.
    row_below = drawn_row + 1
    if row_below > 2:
        # below the bottom row of the level: floor top edge
        rbl_code, rbl_mod = 1, 0
    else:
        rbl_code, _, _, rbl_mod = room.tile(row_below, col - 1)
    row_below_left_code = rbl_code

    t = TILE_TABLE[code]
    lt = TILE_TABLE[left_code]

    # ---- draw_tile_floorright (seg008.c:392) ----
    # Рисуется ТОЛЬКО когда can_see_bottomleft(): текущий тайл —
    # empty/bigpillar_top/doortop/lattice_down.  Иначе topright соседа
    # снизу-слева перекрыт и рисовать его нельзя (иначе «арки» повсюду —
    # это была главная причина замусоренного рендера).
    if code in (0, 9, 12, 26):  # can_see_bottomleft
        _draw_topright(canvas, row_below_left_code, rbl_mod, x, draw_bottom_y,
                       reason=f"topright(below-left tile {row_below_left_code:02X})")
        if lt[4]:  # tile_table[tile_left].floor_right
            # blitters_9_black: ЧЁРНЫЙ силуэт (тень уступа), не светлый спрайт.
            img = load_sprite(42, reason=f"floor-right shadow(left tile {left_code:02X})")
            blit_black(canvas, img, x, draw_main_y + TILE_TABLE[1][5] - (img.height if img else 0) + 1)

    # ---- draw_tile_anim_topright ---- (gate top mask) - skipped: no anim
    # ---- draw_tile_right (seg008.c:456) ----
    if code != 20:  # у самой стены правый край не рисуется
        level_type = 0 if IS_DUNGEON else 1
        if left_code == 20:
            # сосед слева — стена: её правая грань в текущую колонку (chtab_7 id 1)
            img = load_sprite(1, CHTAB_WALL, reason="wall right-face(left tile 14)")
            ry = TILE_TABLE[20][5]  # right_y стены = 2
            blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1)
        elif left_code == 0:
            # сосед слева — empty: фрагмент окна/решётки по его модификатору
            if left_mod <= 3:
                bid = BLUELINE_FRAM1[left_mod]
                if bid:
                    img = load_sprite(bid, reason=f"blueline-window(left empty mod{left_mod})")
                    blit(canvas, img, x,
                         BLUELINE_FRAM_Y[left_mod] + draw_main_y - (img.height if img else 0) + 1)
        elif left_code == 1:
            # сосед слева — floor: правый треугольник (42) + СИЛУЭТ КЛАДКИ (44/45)
            # на задней стене, если модификатор пола != !!level_type.
            img = load_sprite(42, reason="floor-right(part of left tile 01)")
            ry = TILE_TABLE[1][5]  # right_y пола = 2
            blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1)
            num = left_mod if left_mod <= 3 else 0
            if num != level_type:
                bid = BLUELINE_FRAM3[num]
                img = load_sprite(bid, reason=f"brick-silhouette(left floor mod{num})")
                blit(canvas, img, x, draw_main_y - 20 - (img.height if img else 0) + 1)
        else:
            rid = lt[3]  # tile_left.right_id
            if rid:
                ry = lt[5]
                img = load_sprite(rid, reason=f"right(part of left tile {left_code:02X})")
                blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1)
            # stripe: only drawn when tbl_level_type != 0 (palace). Dungeon: skip.
            sid = lt[6]
            if sid and not IS_DUNGEON:
                img = load_sprite(sid, reason=f"stripe(left tile {left_code:02X})")
                blit(canvas, img, x, draw_main_y - 27 - (img.height if img else 0) + 1)

    # ---- draw_tile_anim_right (seg008.c:530) ----
    # «Анимированные» части соседа слева, проецируемые в текущий тайл.
    # Статикой (кадр покоя) рисуем те, что формируют ГЕОМЕТРИЮ фона:
    #   gate  -> решётка (portcullis) на границе с левой комнатой;
    #   loose -> правый треугольный край шаткой плиты (иначе провал в полу);
    #   spike -> статичный кадр шипов по модификатору.
    # torch-flame/chomper — чистая анимация, пропускаем (отдельные sprite_t).
    if left_code == 4:  # tiles_4_gate
        draw_gate_back(canvas, left_mod, draw_xh, draw_bottom_y, draw_main_y)
    elif left_code == 11:  # tiles_11_loose — правый край плиты (loose_fram_right[0]=42)
        img = load_sprite(42, reason="loose right (part of left tile 0B)")
        if img:
            blit(canvas, img, x, (draw_bottom_y - 1) - img.height + 1)
    elif left_code == 2:  # tiles_2_spike — статичный кадр (spikes_fram_right)
        sf = 5 if (left_mod & 0x80) else left_mod
        sid = SPIKES_FRAM_RIGHT[sf] if 0 <= sf < len(SPIKES_FRAM_RIGHT) else 0
        if sid:
            img = load_sprite(sid, reason="spike right (part of left tile 02)")
            if img:
                blit(canvas, img, x, (draw_main_y - 7) - img.height + 1)

    # ---- draw_tile_bottom (seg008.c:570) ----
    if code == 20:
        # Сплошная НИЖНЯЯ грань стены (chtab_7 wall_fram_bottom) + декали.
        wmod = wall_modifier(room, drawn_row, col)
        img = load_sprite(WALL_FRAM_BOTTOM[wmod], CHTAB_WALL, reason="wall bottom-face")
        if img:
            blit(canvas, img, x, draw_bottom_y - img.height + 1)
        wall_pattern(canvas, room, drawn_row, col, 0, 0)
    else:
        bid = t[8]
        img = load_sprite(bid, reason=f"bottom({TILE_NAMES.get(code,'?')})")
        if img:
            blit(canvas, img, x, draw_bottom_y - img.height + 1)

    # ---- draw_loose (seg008.c:600) — статический кадр loose floor ----
    # Нижняя часть шаткой плиты (loose_fram_bottom[0]=43) на draw_bottom_y.
    if code == 11:  # 0x0B loose floor
        img = load_sprite(43, reason="loose bottom (draw_loose)")
        if img:
            blit(canvas, img, x, draw_bottom_y - img.height + 1)

    # ---- draw_tile_base (seg008.c:611) ----
    base_id = t[0]
    base_y = t[2]
    if code == 11:  # loose floor: базовая (верхняя) часть = loose_fram_left[0] = 41
        base_id = 41
    img = load_sprite(base_id, reason=f"base({TILE_NAMES.get(code,'?')})")
    if img:
        blit(canvas, img, x, base_y + draw_main_y - img.height + 1)

    # ---- draw_tile_anim ---- (spike/potion/chomper animated) skipped

    # ---- draw_tile_fore (seg008.c:690) ----
    if code == 20:
        # Основная (средняя) грань стены (chtab_7 wall_fram_main) + декали/марки.
        wmod = wall_modifier(room, drawn_row, col)
        img = load_sprite(WALL_FRAM_MAIN[wmod], CHTAB_WALL, reason="wall main-face")
        if img:
            blit(canvas, img, x, draw_main_y - img.height + 1)
        wall_pattern(canvas, room, drawn_row, col, 1, 1)
    fid = t[9]
    if fid:
        # fore_x — в единицах xh (×8 пикс!), не в пикселях: seg008.c:743
        # xh = fore_x + draw_xh, затем x = xh*8.  fore_y — в пикселях.
        fx = t[10] * 8 + x
        fy = t[11] + draw_main_y
        img = load_sprite(fid, reason=f"fore({TILE_NAMES.get(code,'?')})")
        if img:
            blit(canvas, img, fx, fy - img.height + 1)

    # Save per-tile log (keyed by logical room position: row=drawn_row, col)
    TILE_LOGS[(drawn_row, col)] = list(CURRENT_TILE_LOG)


def _draw_topright(canvas, tiletype, modifier, x, draw_bottom_y, reason=""):
    if tiletype in (7, 12):  # doortop_with_floor / doortop -> palace uses frame
        return
    elif tiletype == 20:  # wall
        img = load_sprite(2, CHTAB_WALL, reason=reason)
    else:
        tid = TILE_TABLE[tiletype][7]
        if tid == 0:
            return
        img = load_sprite(tid, reason=reason)
    if img:
        blit(canvas, img, x, draw_bottom_y - img.height + 1)


def render_room(room_num):
    with open(os.path.join(LEVEL_DIR, f"res{2000 + room_num}.bin"), "rb") as f:
        data = f.read()
    level = Level(data)
    room = Room(level, room_num)
    # load_leftroom(): левая комната по линку L — её col 9 виден на границе
    # (col 0) текущей комнаты (напр. решётка ворот из соседней комнаты).
    left_link = level.links[room_num][0]
    if left_link:
        room.left_room = Room(level, left_link)

    canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0))
    # rows drawn 2,1,0 (bottom first)
    for drawn_row in (2, 1, 0):
        for col in range(10):
            draw_tile(canvas, room, drawn_row, col)

    return canvas, room, level


def render_single_tile(code, modifier=0, left_code=20, below_left_code=1):
    """Render one tile in isolation on a 40x64 canvas (matching SDLPoP tile box)."""
    TILE_W, TILE_H = 40, 64
    canvas = Image.new("RGBA", (TILE_W, TILE_H), (0, 0, 0, 0))
    # Place tile as if it were at drawn_row=2 (bottom), col=0
    drawn_row = 2
    col = 0
    # Build a fake Room backed by single values
    class FakeRoom:
        def tile(self, r, c):
            if c < 0:
                return left_code, 0, 0, 0
            if r < 0:
                return 1, 0, 0, 0
            if r == drawn_row and c == 0:
                return code, 0, 0, modifier
            return below_left_code, 0, 0, 0
    room = FakeRoom()
    draw_tile(canvas, room, drawn_row, col, left_edge_code=left_code)
    return canvas


def main():
    import sys
    room_num = 1
    if len(sys.argv) > 1:
        room_num = int(sys.argv[1])

    canvas, room, level = render_room(room_num)
    out = f"./room{room_num}_render.png"
    # Прозрачный фон (задняя чёрная стена, которую мы не рисуем) -> ЧЁРНЫМ,
    # а не белым как показывает вьюер прозрачность.  Композитим по альфе.
    flat = Image.new("RGB", canvas.size, (0, 0, 0))
    flat.paste(canvas, (0, 0), canvas)
    flat.save(out)
    print(f"Saved {out} ({canvas.width}x{canvas.height})")

    # debug grid with code labels (на чёрном фоне)
    from PIL import ImageDraw
    dbg = flat.copy()
    d = ImageDraw.Draw(dbg)
    for r in range(3):
        by = 63 * r + 65
        for c in range(10):
            x = COL_XH[c] * 8
            d.rectangle([x, by - 63, x + 31, by], outline=(255, 0, 0), width=1)
            code, _, _, _ = room.tile(r, c)
            d.text((x + 2, by - 60), f"{code:02X}", fill=(255, 255, 0))
    dbg.save(f"./room{room_num}_debug.png")
    print(f"Saved room{room_num}_debug.png")

    # Per-tile individual renders (the 30 tiles of the room)
    # For proper neighbour context, render each tile inside its real room,
    # then crop its 32x64 (40x64) column box.
    tiles_dir = f"./tiles_room{room_num}"
    os.makedirs(tiles_dir, exist_ok=True)
    for r in range(3):
        by = 63 * r + 65
        for c in range(10):
            x = COL_XH[c] * 8
            code, _, _, _ = room.tile(r, c)
            # crop the column box (40 wide to include left overlap, 64 tall above bottom)
            box = (x, by - 64, x + 40, by)
            tile_img = canvas.crop(box)
            tile_img.save(os.path.join(tiles_dir, f"r{r}c{c}_{code:02X}.png"))
    print(f"Saved 30 tile crops to {tiles_dir}/")
    # Per-tile report: which images each tile pulls and why.
    print()
    print("=" * 78)
    print(f"PER-TILE SPRITE REPORT  (room {room_num}, {'DUNGEON' if IS_DUNGEON else 'PALACE'})")
    print("=" * 78)
    for r in range(3):
        for c in range(10):
            code, fg_byte, mbit, bb = room.tile(r, c)
            name = TILE_NAMES.get(code, f"?0x{code:02X}")
            btd = level.backtable_desc(code, bb)
            line = (f"\n[{r},{c}] code=0x{code:02X} ({name})  "
                    f"fg=0x{fg_byte:02X} mbit={mbit} backtable=0x{bb:02X} ({btd})")
            # If this tile is an event activator (group 'event'), show the door it drives.
            if code in (0x06, 0x0F):
                scr, loc, trig = level.door_for_event(bb)
                line += f"\n    -> DOOR EVENT id={bb}: screen={scr} loc={loc} trigger_next={trig}"
            print(line)
            logs = TILE_LOGS.get((r, c), [])
            if not logs:
                print("    (no sprites drawn)")
            for sid, source, reason in logs:
                print(f"    res{200+sid:03d}.png  <- {source:8s}  {reason}")
    print()
    print("=" * 78)

    # Level-level summary: start position, links, door events, guards.
    print()
    print("LEVEL STRUCTURE SUMMARY")
    print("-" * 78)
    print(f"start position: room={level.start[0]} loc={level.start[1]} dir={level.start[2]}")
    links = level.links.get(room_num, (0, 0, 0, 0))
    print(f"room {room_num} links: L={links[0]} R={links[1]} Up={links[2]} Dn={links[3]}")
    # guard in this room
    gi = room_num - 1
    gloc = level.guard_loc[gi]
    if gloc < 30:
        gr, gc = gloc // 10, gloc % 10
        print(f"guard: loc=tile(r{gr},c{gc}) dir={level.guard_dir[gi]} "
              f"skill={level.guard_skill[gi]} colour={level.guard_colour[gi]}")
    else:
        print("guard: none")
    # door events driven by any tile in this room
    print("door events referenced by this room's tiles:")
    seen = set()
    for off in range(30):
        fb, code, mbit = room.fg[off]
        bb = room.bg[off]
        if code in (0x06, 0x0F):
            if bb not in seen:
                seen.add(bb)
                scr, loc, trig = level.door_for_event(bb)
                print(f"  event id={bb} (tile off {off}): screen={scr} loc={loc} trigger_next={trig}")
    if not seen:
        print("  (none)")

    print()
    print("Room", room_num, "foreground codes (3 rows x 10 cols):")
    for r in range(3):
        print("  ", " ".join(f"{room.tile(r,c)[0]:02X}" for c in range(10)))


if __name__ == "__main__":
    main()
