'''
Shared decode library for Kellogg's Tony & Friends asset formats.
Ported (not copied) from the C# reference project
https://github.com/movAX13h/tony-and-friends-in-kelloggs-land (no LICENSE file,
therefore reimplemented from scratch in Python using their format docs as a guide).

Container: PCKELL.DAT holds all 178 assets, footer-indexed (see DATContainer).
'''
import struct
import os
from collections import namedtuple

# ---------------------------------------------------------------------------
# Container (PCKELL.DAT)
# ---------------------------------------------------------------------------

class DATContainer:
    def __init__(self, path):
        with open(path, 'rb') as f:
            self.data = f.read()
        self.entries = {}  # filename -> bytes
        self._parse()

    def _parse(self):
        data = self.data
        num_entries = struct.unpack_from('<H', data, len(data) - 4)[0] + 1
        offset = struct.unpack_from('<i', data, len(data) - 8)[0]
        index_offset = offset

        raw = []
        for _ in range(num_entries):
            name_len = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            filename = data[offset:offset + name_len].decode('ascii', errors='replace')
            offset += name_len
            e_offset = struct.unpack_from('<i', data, offset)[0]
            offset += 4
            raw.append([filename, e_offset])

        for i, e in enumerate(raw):
            length = (raw[i + 1][1] if i + 1 < len(raw) else index_offset) - e[1]
            self.entries[e[0]] = data[e[1]:e[1] + length]

    def extract_all(self, out_dir):
        os.makedirs(out_dir, exist_ok=True)
        for name, payload in self.entries.items():
            with open(os.path.join(out_dir, name), 'wb') as f:
                f.write(payload)


# ---------------------------------------------------------------------------
# Palette (from a .PCC / PCX file's trailing 256-color block)
# ---------------------------------------------------------------------------

def get_pcx_palette(pcc_bytes):
    '''Returns list of 256 (r,g,b) tuples from a PCX v5 file's end-of-file palette.'''
    marker = pcc_bytes[-769]
    if marker != 0x0C:
        raise ValueError('PCX palette marker (0x0C) not found')
    pal_bytes = pcc_bytes[-768:]
    return [tuple(pal_bytes[i:i+3]) for i in range(0, 768, 3)]


def load_pcc(pcc_bytes):
    '''Decode a .PCC (= plain PCX v5, 8bpp, RLE, own trailing 256-color
    palette) via Pillow's battle-tested PCX decoder instead of a hand-rolled
    RLE parser -- see git history 2026-07-22 (commits 5efba4b/62af15e/3410a53)
    for why: TWO from-scratch RLE implementations silently produced visibly
    wrong pixels (shifted/duplicated content) for full-screen 320x200 images
    (KARTE.PCC, KELLOGGS.PCC) despite being "verified" -- Pillow avoids that
    whole class of bug entirely.

    Returns (width, height, indices, palette):
      indices  -- flat list[int] of palette indices, len width*height, row-major
      palette  -- list of 256 (r,g,b) tuples (this file's OWN embedded palette,
                  not any global one)
    '''
    from PIL import Image
    import io
    img = Image.open(io.BytesIO(pcc_bytes))
    img.load()
    if img.mode != 'P':
        raise ValueError(f'expected palette (P) mode PCX, got {img.mode}')
    width, height = img.size
    indices = list(img.getdata())
    raw_pal = img.getpalette()  # flat [r,g,b, r,g,b, ...], may be shorter than 768
    raw_pal = (raw_pal + [0] * 768)[:768]
    palette = [tuple(raw_pal[i:i+3]) for i in range(0, 768, 3)]
    return width, height, indices, palette


def pcc_to_rgba(pcc_bytes, transparent_index=None):
    '''Convenience wrapper around load_pcc(): returns (width, height, rgba_bytes)
    ready for e.g. pygame.image.frombuffer(rgba, (w,h), 'RGBA') or PIL Image.frombytes.
    If transparent_index is given, that palette index becomes alpha=0.'''
    width, height, indices, palette = load_pcc(pcc_bytes)
    out = bytearray(width * height * 4)
    for i, idx in enumerate(indices):
        r, g, b = palette[idx & 0xFF]
        a = 0 if (transparent_index is not None and idx == transparent_index) else 255
        out[i*4:i*4+4] = bytes([r, g, b, a])
    return width, height, bytes(out)


def get_bob_palette(entries, name):
    '''name = basename without extension, e.g. 'TONY' for TONY.BOB.
    Implements the palette-selection rules from Form1.cs (BOB case).'''
    pcc_name = name + '.PCC'
    if pcc_name in entries:
        return list(get_pcx_palette(entries[pcc_name]))
    if len(name) == 1:
        # the ants have names like A.BOB .. O.BOB
        return list(get_pcx_palette(entries['ANTS.PCC']))
    pal = list(get_pcx_palette(entries['W2.PCC']))
    pal[0] = (0, 0, 0)
    return pal


def get_world_palette(entries, world):
    '''world = 'W1'|'W2'|'W3'. Implements ICO/MAP palette rule (addW2Palette):
    W2.PCC carries 16 shared colors (indices 16..31) used for animations/items
    across all worlds, so W1/W3 palettes borrow that slice from W2.'''
    pal = list(get_pcx_palette(entries[world + '.PCC']))
    if world != 'W2':
        w2 = get_pcx_palette(entries['W2.PCC'])
        for i in range(16):
            pal[16 + i] = w2[16 + i]
    return pal


# ---------------------------------------------------------------------------
# BOB (animated sprites: self-modifying x86 'draw code' + embedded pixel data)
# ---------------------------------------------------------------------------

CopyInstr = namedtuple('CopyInstr', ['ega_page', 'offset', 'data'])

def _parse_bob_executable(data):
    '''Interprets the tiny x86 instruction stream used to blit pixels.
    Returns list of CopyInstr, or None if an unrecognized opcode is hit.'''
    instrs = []
    pc = 0
    ega_page = -1
    n = len(data)
    while True:
        if pc >= n:
            return None
        op = data[pc]
        if op == 0x03:  # add si, cx (0x03 0xF1)
            if data[pc+1] != 0xF1:
                return None
            pc += 2
        elif op == 0xCB:  # retf
            pc += 1
            break
        elif op in (0x56, 0x58, 0x5E, 0x50):  # push si / push ax / pop si / push ax
            pc += 1
        elif op == 0xEE:  # out dx, al  -> advance EGA page (cycles 0..3)
            ega_page = (ega_page + 1) & 3
            pc += 1
        elif op == 0xD0:  # rol al, 1  (0xD0 0xC0)
            if data[pc+1] != 0xC0:
                return None
            pc += 2
        elif op == 0x8A:  # mov cl, ah/bl/bh (no-op for our purposes)
            if data[pc+1] not in (0xCC, 0xCB, 0xCF):
                return None
            pc += 2
        elif op == 0xC6:
            sub = data[pc+1]
            if sub == 0x84:  # mov byte ptr [si+AAAA], BB
                offset = data[pc+2] | (data[pc+3] << 8)
                const = data[pc+4]
                pc += 5
                instrs.append(CopyInstr(ega_page, offset, bytes([const])))
            elif sub == 0x44:  # mov byte ptr [si+AA], BB
                offset = data[pc+2]
                const = data[pc+3]
                pc += 4
                instrs.append(CopyInstr(ega_page, offset, bytes([const])))
            else:
                return None
        elif op == 0xC7:
            sub = data[pc+1]
            if sub == 0x44:  # mov word ptr [si+AA], BBBB
                offset = data[pc+2]
                const = data[pc+3] | (data[pc+4] << 8)
                pc += 5
                instrs.append(CopyInstr(ega_page, offset, bytes([const & 0xFF, (const >> 8) & 0xFF])))
            elif sub == 0x84:  # mov word ptr [si+AAAA], BBBB
                offset = data[pc+2] | (data[pc+3] << 8)
                const = data[pc+4] | (data[pc+5] << 8)
                pc += 6
                instrs.append(CopyInstr(ega_page, offset, bytes([const & 0xFF, (const >> 8) & 0xFF])))
            else:
                return None
        else:
            return None
    return instrs


BOB_STRIDE = 84

class BobFrame:
    __slots__ = ('width', 'height', 'pixels')  # pixels: list of palette indices, width*height, -1 = transparent
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.pixels = [-1] * (width * height)

    def set(self, x, y, idx):
        if 0 <= x < self.width and 0 <= y < self.height:
            self.pixels[y * self.width + x] = idx


def parse_bob(data):
    '''Returns list of BobFrame (palette-index pixel grids, -1 = transparent/unset).'''
    frames = []
    pos = 0
    n = len(data)
    while pos < n:
        header = data[pos:pos+14]
        if len(header) < 14:
            raise ValueError(f'truncated header at {pos}')
        width = struct.unpack_from('<h', header, 6)[0]
        height = struct.unpack_from('<h', header, 8)[0]
        ptr_seg_len = struct.unpack_from('<h', header, 10)[0]
        pos += 14

        pointers_data = data[pos:pos+ptr_seg_len]
        pos += ptr_seg_len

        last_instruction = struct.unpack_from('<h', pointers_data, len(pointers_data) - 2)[0]
        next_frame_pos = pos + last_instruction
        exec_segment = data[pos:next_frame_pos]
        pos = next_frame_pos

        instrs = _parse_bob_executable(exec_segment)
        if instrs is None:
            raise ValueError(f'failed to parse exec segment for frame {len(frames)} (len={len(exec_segment)})')

        frame = BobFrame(width, height)
        for instr in instrs:
            for i, byte in enumerate(instr.data):
                p = instr.offset + i
                x = (p % BOB_STRIDE) * 4 + instr.ega_page
                y = p // BOB_STRIDE
                idx = byte - 0x80
                frame.set(x, y, idx)
        frames.append(frame)

    if pos != n:
        raise ValueError(f'missed {n - pos} extra bytes at end of file')

    return frames


# ---------------------------------------------------------------------------
# ICO (16x16 tiles, EGA scrambled pixel order)
# ---------------------------------------------------------------------------

def parse_ico(data):
    '''Returns list of 16x16 palette-index grids (flat lists, len 256).'''
    num_tiles = (len(data) - 1) // 256
    tiles = []
    ptr = 0
    for _ in range(num_tiles):
        grid = [0] * 256
        for y in range(16):
            for page in range(4):
                for x in range(4):
                    k = data[ptr]
                    ptr += 1
                    col = x * 4 + page
                    grid[y * 16 + col] = k - 128
        tiles.append(grid)
    return tiles


# ---------------------------------------------------------------------------
# MAP (grid of tile refs + collision type, big-endian!)
# ---------------------------------------------------------------------------

def parse_map(data):
    if data[0:4] != b'TLE1':
        raise ValueError('MAP signature TLE1 not found')
    width = struct.unpack_from('>h', data, 4)[0]
    height = struct.unpack_from('>h', data, 6)[0]
    unknown = struct.unpack_from('>h', data, 8)[0]
    if unknown != 9:
        raise ValueError(f'unexpected constant {unknown} (expected 9)')

    pos = 10
    cells = []  # row-major, len width*height, each (tile, type)
    for _ in range(width * height):
        value = struct.unpack_from('>H', data, pos)[0]
        pos += 2
        tile = value & 0x1FF
        ctype = value >> 9
        cells.append((tile, ctype))

    if pos != len(data):
        raise ValueError(f'missed {len(data) - pos} extra bytes at end of MAP')

    return width, height, cells
