#!/usr/bin/env python3
"""Build a focused review of the DirectDraw palette buffer/copy/apply path."""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from probe_exe_scene_tables import read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
EXE = ROOT / "Hwanse2.exe"


def hx(value: int | None) -> str | None:
    if value is None:
        return None
    return f"0x{value:08x}"


def exe_bytes(exe: bytes, sections: list[dict[str, Any]], va: int, size: int = 16) -> dict[str, Any]:
    offset = va_to_offset(sections, va)
    if offset is None:
        return {
            "vaHex": hx(va),
            "fileOffsetHex": None,
            "bytes": None,
            "available": False,
        }
    blob = exe[offset : offset + size]
    return {
        "vaHex": hx(va),
        "fileOffsetHex": hx(offset),
        "bytes": blob.hex(" "),
        "available": True,
    }


def byte_evidence(exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows = [
        (
            0x00415C58,
            8,
            "palette header version",
            "writes 0x0300 to the palette header at 0x004676e4",
        ),
        (
            0x00415C62,
            8,
            "palette entry count",
            "writes 0x0100 to palette header +2, grounding 256 entries",
        ),
        (
            0x00415C68,
            14,
            "palette entries pointer",
            "sets 0x004676e8 to palette header +4, the first PALETTEENTRY byte",
        ),
        (
            0x00415CB6,
            10,
            "PC flag byte loop",
            "writes byte 0x04 to entry flag byte for entries 0x0a..0xf5",
        ),
        (
            0x0041652E,
            8,
            "CreatePalette call",
            "calls IDirectDraw::CreatePalette through vtable +0x14",
        ),
        (
            0x00416555,
            8,
            "primary SetPalette call",
            "attaches the created palette to the primary surface through vtable +0x7c",
        ),
        (
            0x00416599,
            8,
            "range apply wrapper",
            "calls the SetEntries helper for range start 0 count 0xff",
        ),
        (
            0x00416621,
            15,
            "SetEntries source pointer",
            "computes 0x004676e8 + start * 4 as the PALETTEENTRY source pointer",
        ),
        (
            0x00416649,
            8,
            "SetEntries call A",
            "calls IDirectDrawPalette::SetEntries through vtable +0x18",
        ),
        (
            0x00416677,
            8,
            "SetEntries helper B",
            "second range-apply helper used by VM/deferred palette paths",
        ),
        (
            0x00416708,
            8,
            "SetEntries call B",
            "calls IDirectDrawPalette::SetEntries through vtable +0x18",
        ),
        (
            0x0041677A,
            14,
            "copy red channel",
            "copies caller/source red byte into the current palette entry buffer",
        ),
        (
            0x004167A0,
            14,
            "copy green channel",
            "copies caller/source green byte into the current palette entry buffer",
        ),
        (
            0x004167C4,
            14,
            "copy blue channel",
            "copies caller/source blue byte into the current palette entry buffer",
        ),
        (
            0x004167F1,
            8,
            "copy then apply",
            "calls the range SetEntries helper after RGB bytes are copied",
        ),
        (
            0x00416856,
            8,
            "GetEntries call",
            "reads current palette entries through IDirectDrawPalette::GetEntries",
        ),
    ]
    return [
        {**exe_bytes(exe, sections, va, size), "label": label, "meaning": meaning}
        for va, size, label, meaning in rows
    ]


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    byte_rows = byte_evidence(exe, sections)
    byte_by_va = {str(row["vaHex"]).lower(): row for row in byte_rows}

    direct_draw_palette_calls = [
        {
            "vaHex": "0x0041652e",
            "method": "IDirectDraw::CreatePalette",
            "vtableOffsetHex": "0x14",
            "arguments": "flags=0x04, entries=0x004676e8, out=0x004676c4",
            "role": "creates the 8-bit DirectDraw palette object from the EXE-owned entry buffer",
        },
        {
            "vaHex": "0x00416555",
            "method": "IDirectDrawSurface::SetPalette",
            "vtableOffsetHex": "0x7c",
            "arguments": "surface=primary wrapper at 0x0055abd8, palette=0x004676c4",
            "role": "attaches the DirectDraw palette to the primary display surface",
        },
        {
            "vaHex": "0x00416649",
            "method": "IDirectDrawPalette::SetEntries",
            "vtableOffsetHex": "0x18",
            "arguments": "start/count from helper args, entries=0x004676e8 + start*4",
            "role": "applies a current-palette range after computing the entry pointer",
        },
        {
            "vaHex": "0x00416708",
            "method": "IDirectDrawPalette::SetEntries",
            "vtableOffsetHex": "0x18",
            "arguments": "start/count from helper args, entries=0x004676e8 + start*4",
            "role": "second range apply path used after palette VM/list writes",
        },
        {
            "vaHex": "0x00416856",
            "method": "IDirectDrawPalette::GetEntries",
            "vtableOffsetHex": "0x20",
            "arguments": "reads entries into EXE buffer",
            "role": "palette snapshot/readback helper",
        },
    ]

    update_routines = [
        {
            "vaHex": "0x00415c53",
            "name": "palette buffer initializer",
            "effect": "initializes a DD-style palette header, 256-entry count, entry pointer, and PC flags for entries 0x0a..0xf5",
            "status": "grounded",
        },
        {
            "vaHex": "0x004165b8",
            "name": "range SetEntries helper A",
            "effect": "optionally reattaches the palette to the primary surface, then applies a range from 0x004676e8 + start*4",
            "status": "grounded",
        },
        {
            "vaHex": "0x00416677",
            "name": "range SetEntries helper B",
            "effect": "same range-apply class as A; this one is reached by deferred palette update paths",
            "status": "grounded",
        },
        {
            "vaHex": "0x00416736",
            "name": "RGB copy then SetEntries",
            "effect": "copies count RGB triples into the current palette entry buffer, then calls the range apply helper",
            "status": "grounded",
        },
    ]

    return {
        "kind": "hwanse-palette-pipeline-review",
        "status": "palette-buffer-copy-apply-path-grounded",
        "source": [
            "Hwanse2.exe",
            "out/directdraw_flow.json",
            "web/map_animation_execution_boundary_review.html",
            "tools/build_map_animation_tile_review.py",
        ],
        "summary": {
            "displayMode": "640x480x8 indexed color",
            "paletteObjectGlobal": "0x004676c4",
            "paletteHeaderGlobal": "0x004676e4",
            "paletteEntriesGlobal": "0x004676e8",
            "entryCount": 256,
            "entryStrideBytes": 4,
            "entryLayout": "R, G, B, flags",
            "pcFlagRange": "0x0a..0xf5",
            "pcFlagValue": "0x04",
            "setEntriesHelpers": ["0x004165b8", "0x00416677"],
            "copyThenApplyHelper": "0x00416736",
            "byteEvidenceRows": len(byte_rows),
            "coreConclusion": (
                "The EXE keeps a mutable 256-entry palette buffer at 0x004676e8. "
                "Palette effects copy or step RGB bytes inside that buffer, then push a range to DirectDraw with SetEntries. "
                "This is a color/fade/ramp pipeline, not proof of tile-position animation."
            ),
        },
        "memoryModel": [
            {
                "addressHex": "0x004676c4",
                "name": "DirectDraw palette object pointer",
                "meaning": "receives the IDirectDrawPalette pointer created by CreatePalette",
            },
            {
                "addressHex": "0x004676e4",
                "name": "palette header/allocation",
                "meaning": "stores version 0x0300 and entry count 0x0100 before the entry bytes",
            },
            {
                "addressHex": "0x004676e8",
                "name": "current palette entries",
                "meaning": "first mutable PALETTEENTRY; entry i is 0x004676e8 + i*4",
            },
            {
                "addressHex": "0x00559d98",
                "name": "palette dirty flag",
                "meaning": "map/VM palette handlers mark pending palette changes before the deferred SetEntries apply",
            },
        ],
        "directDrawPaletteCalls": direct_draw_palette_calls,
        "updateRoutines": update_routines,
        "paletteVmBoundary": {
            "knownOpcodeFamilies": ["0x38", "0x39"],
            "confirmedMeaning": (
                "0x38 steps palette entries toward target/list/backup RGB values; "
                "0x39 writes/fills current or backup palette ranges/lists."
            ),
            "deferredApply": "if dirty flag 0x00559d98 is set, the VM path applies start 0 count 0x100 through 0x00416677 and clears the flag",
            "boundary": "these handlers explain palette color transforms and fades, but do not identify per-map waterfall/fire tile source-index motion",
        },
        "byteEvidence": byte_rows,
        "verification": {
            "hasExe": EXE.exists(),
            "paletteHeaderWriteAvailable": byte_by_va.get("0x00415c58", {}).get("available") is True,
            "createPaletteCallAvailable": byte_by_va.get("0x0041652e", {}).get("available") is True,
            "setEntriesCallAAvailable": byte_by_va.get("0x00416649", {}).get("available") is True,
            "setEntriesCallBAvailable": byte_by_va.get("0x00416708", {}).get("available") is True,
            "copyThenApplyAvailable": byte_by_va.get("0x004167f1", {}).get("available") is True,
            "notMapTileMotionProof": True,
        },
        "modelBoundaries": [
            {
                "area": "palette fade/ramp",
                "conclusion": "range/list RGB writes plus SetEntries are grounded and should be treated as the EXE palette animation path",
                "status": "promoted",
            },
            {
                "area": "indexed CNS rendering",
                "conclusion": "web rendering should preserve palette-index semantics where fidelity matters; precolored immutable PNGs cannot model later SetEntries updates",
                "status": "implementation-guidance",
            },
            {
                "area": "map animated tiles",
                "conclusion": "palette changes can recolor indexed pixels, but visible waterfall/fire source-tile motion still needs a separate tile/source-index producer",
                "status": "boundary-fixed",
            },
        ],
        "openQuestions": [
            "Name the exact callers that feed palette opcode 0x38/0x39 streams for each scene/map/effect root.",
            "Find the non-palette producer for map tile source-index motion if waterfall/fire animation is not purely palette-based.",
            "Capture or statically bind which palette ranges are used by individual battle helper effects that are palette-only.",
        ],
    }


def main() -> int:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    (OUT / "palette_pipeline_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
