#!/usr/bin/env python3
"""Decode the repeated object-script payload at 0x00442c75.

This is intentionally narrow.  The payload is the only valid repeated
``opcode 0x5e mode=1`` payload found by ``summarize_object_script_payload_producer``.
The goal is to decide whether this payload proves a field-map route, or whether
it is a local active-object/display script.
"""
from __future__ import annotations

import argparse
import html
import json
import struct
from pathlib import Path
from typing import Any

from probe_exe_scene_tables import find_cns_strings, read_sections, va_to_offset


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

PAYLOAD_VA = 0x00442C75
SCRIPT_CURSOR_VA = 0x00442C9D
FRAME_TABLE_VA = 0x00442CF5
ANIM_FRAME_SCRIPT_VA = 0x00442CFD
IDLE_FRAME_SCRIPT_VA = 0x00442D25
GENERIC_HANDLER_TABLE = 0x00440538
MAP_LOADER_FUNCTION = 0x0042449C
INPUT_STATE_BASE = 0x0059E310

HANDLER_NAMES = {
    0x00: "destroy-object",
    0x01: "stop-tick",
    0x03: "jump",
    0x08: "object-field-initializer",
    0x10: "byte-expression-store",
    0x13: "byte-conditional-branch",
    0x18: "dword-expression-store",
    0x20: "attach-frame-script",
    0x21: "frame-step",
    0x40: "bind-input-state-base",
    0x5E: "transient-object-helper",
}


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


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


def read_at(exe: bytes, sections: list[dict], va: int, size: int) -> bytes:
    offset = va_to_offset(sections, va)
    if offset is None:
        raise ValueError(f"VA {hex32(va)} is outside raw sections")
    return exe[offset: offset + size]


def u8(exe: bytes, sections: list[dict], va: int) -> int:
    return read_at(exe, sections, va, 1)[0]


def u16(exe: bytes, sections: list[dict], va: int) -> int:
    return struct.unpack("<H", read_at(exe, sections, va, 2))[0]


def u32(exe: bytes, sections: list[dict], va: int) -> int:
    return struct.unpack("<I", read_at(exe, sections, va, 4))[0]


def handler_for(exe: bytes, sections: list[dict], opcode: int) -> int:
    return u32(exe, sections, GENERIC_HANDLER_TABLE + opcode * 4)


def is_default_handler(exe: bytes, sections: list[dict], opcode: int) -> bool:
    return handler_for(exe, sections, opcode) == 0x0040239F


def decode_payload_header(exe: bytes, sections: list[dict]) -> dict[str, Any]:
    dwords = [u32(exe, sections, PAYLOAD_VA + index * 4) for index in range(8)]
    return {
        "payloadVa": PAYLOAD_VA,
        "payloadVaHex": hex32(PAYLOAD_VA),
        "dwords": dwords,
        "dwordsHex": [hex32(value) for value in dwords],
        "resourceOpenArgVa": dwords[0],
        "resourceOpenArgVaHex": hex32(dwords[0]),
        "resourceCleanupArgVa": dwords[1],
        "resourceCleanupArgVaHex": hex32(dwords[1]),
        "scriptCursorVa": dwords[2],
        "scriptCursorVaHex": hex32(dwords[2]),
        "interpretation": [
            "payload[+0] is passed to 0x00423a2f by opcode 0x5e mode 1",
            "payload[+4] is passed to 0x00423a2f by opcode 0x5e mode 0 cleanup",
            "payload[+8] is the new object's +0x40 script cursor",
        ],
        "rawHex": read_at(exe, sections, PAYLOAD_VA, 0x80).hex(" "),
    }


def decode_initializer(exe: bytes, sections: list[dict], va: int) -> tuple[dict[str, Any], int]:
    cursor = va + 4
    writes: list[dict[str, Any]] = []
    while True:
        subcmd = u8(exe, sections, cursor)
        if subcmd == 0xFF:
            return {
                "va": va,
                "vaHex": hex32(va),
                "opcodeHex": "0x08",
                "opcodeName": HANDLER_NAMES[0x08],
                "length": cursor + 4 - va,
                "writes": writes,
                "summary": "object field initializer; includes active/scripted gate object +0x14 = 0x0101",
            }, cursor + 4
        field = u8(exe, sections, cursor + 1)
        if subcmd == 1:
            value = u8(exe, sections, cursor + 2)
            writes.append({
                "width": 1,
                "fieldOffset": field,
                "field": f"+0x{field:02x}",
                "value": value,
                "valueHex": hex8(value),
            })
            cursor += 4
        elif subcmd == 2:
            value = u16(exe, sections, cursor + 2)
            writes.append({
                "width": 2,
                "fieldOffset": field,
                "field": f"+0x{field:02x}",
                "value": value,
                "valueHex": f"0x{value:04x}",
            })
            cursor += 4
        elif subcmd == 3:
            value = u32(exe, sections, cursor + 4)
            writes.append({
                "width": 4,
                "fieldOffset": field,
                "field": f"+0x{field:02x}",
                "value": value,
                "valueHex": hex32(value),
            })
            cursor += 8
        else:
            writes.append({
                "width": 0,
                "fieldOffset": field,
                "field": f"+0x{field:02x}",
                "value": None,
                "valueHex": "",
                "note": f"unknown subcommand {hex8(subcmd)}; handler advances by 4",
            })
            cursor += 4


def decode_frame_script(exe: bytes, sections: list[dict], va: int, max_steps: int = 8) -> dict[str, Any]:
    rows: list[dict[str, Any]] = []
    cursor = va
    loop_target = None
    for _ in range(max_steps):
        opcode = u8(exe, sections, cursor)
        if opcode == 0x21:
            gate = u16(exe, sections, cursor + 2)
            frame = u16(exe, sections, cursor + 4)
            sprite = u16(exe, sections, cursor + 6)
            rows.append({
                "va": cursor,
                "vaHex": hex32(cursor),
                "opcodeHex": "0x21",
                "gate": gate,
                "frameIndex": frame,
                "spriteIndex": sprite,
                "spriteIndexHex": f"0x{sprite:04x}",
                "summary": f"frame {frame} on sprite 0x{sprite:04x}, gate {gate}",
            })
            cursor += 8
        elif opcode == 0x03:
            target = u32(exe, sections, cursor + 4)
            loop_target = target
            rows.append({
                "va": cursor,
                "vaHex": hex32(cursor),
                "opcodeHex": "0x03",
                "targetVa": target,
                "targetVaHex": hex32(target),
                "summary": f"jump -> {hex32(target)}",
            })
            break
        else:
            rows.append({
                "va": cursor,
                "vaHex": hex32(cursor),
                "opcodeHex": hex8(opcode),
                "summary": "unexpected frame-script opcode",
            })
            break
    return {
        "startVa": va,
        "startVaHex": hex32(va),
        "rows": rows,
        "loopTargetVa": loop_target,
        "loopTargetVaHex": hex32(loop_target),
        "loopsToSelf": loop_target == va,
        "frameSequence": [row["frameIndex"] for row in rows if "frameIndex" in row],
        "gateSequence": [row["gate"] for row in rows if "gate" in row],
    }


def decode_object_script(exe: bytes, sections: list[dict]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    cursor = SCRIPT_CURSOR_VA
    for _ in range(32):
        opcode = u8(exe, sections, cursor)
        base: dict[str, Any] = {
            "va": cursor,
            "vaHex": hex32(cursor),
            "opcode": opcode,
            "opcodeHex": hex8(opcode),
            "opcodeName": HANDLER_NAMES.get(opcode, "default-handler" if is_default_handler(exe, sections, opcode) else "unknown"),
            "handlerVaHex": hex32(handler_for(exe, sections, opcode)),
        }
        if opcode == 0x08:
            row, cursor = decode_initializer(exe, sections, cursor)
            row.update({key: base[key] for key in ("handlerVaHex",)})
            rows.append(row)
            continue
        if opcode == 0x40:
            row = {
                **base,
                "length": 4,
                "writes": [{"field": "+0xa8", "valueHex": hex32(INPUT_STATE_BASE)}],
                "summary": f"bind object +0xa8 to input state base {hex32(INPUT_STATE_BASE)}",
            }
            rows.append(row)
            cursor += 4
            continue
        if opcode == 0x20:
            target = u32(exe, sections, cursor + 4)
            row = {
                **base,
                "length": 8,
                "writes": [
                    {"field": "+0x64", "valueHex": hex32(target)},
                    {"field": "+0x00 flags", "valueHex": "OR 0x04000000"},
                    {"field": "+0x62", "valueHex": "0x0001"},
                ],
                "frameScriptVa": target,
                "frameScriptVaHex": hex32(target),
                "summary": f"attach frame/timer script {hex32(target)}",
            }
            rows.append(row)
            cursor += 8
            continue
        if opcode == 0x18:
            mode = u8(exe, sections, cursor + 1)
            dest_offset = u8(exe, sections, cursor + 2)
            input_selector = u8(exe, sections, cursor + 3)
            table = u32(exe, sections, cursor + 4)
            entries = [u32(exe, sections, table + index * 4) for index in range(2)]
            row = {
                **base,
                "length": 8,
                "modeHex": hex8(mode),
                "operation": "set",
                "destination": f"object +0x{dest_offset:02x}",
                "source": f"dword table indexed by byte [object+0xa8 + 0x{input_selector:02x}]",
                "tableVa": table,
                "tableVaHex": hex32(table),
                "tableEntriesHex": [hex32(value) for value in entries],
                "summary": (
                    f"set object +0x{dest_offset:02x} from table {hex32(table)} "
                    f"using input-state byte +0x{input_selector:02x}"
                ),
            }
            rows.append(row)
            cursor += 8
            continue
        if opcode == 0x5E:
            mode = u8(exe, sections, cursor + 1)
            if mode == 2:
                summary = "run current object +0x64 frame script once and update display position"
            elif mode == 3:
                summary = "initialize helper/display position from current actor and input/viewport state"
            elif mode == 0:
                summary = "cleanup transient helper object"
            elif mode == 1:
                summary = "spawn transient helper object from payload"
            else:
                summary = "unknown 0x5e mode"
            rows.append({
                **base,
                "length": 4,
                "mode": mode,
                "modeHex": hex8(mode),
                "summary": summary,
            })
            cursor += 4
            continue
        if opcode == 0x01:
            rows.append({
                **base,
                "length": 4,
                "summary": "stop this object-script tick; next tick resumes after this command",
            })
            cursor += 4
            continue
        if opcode == 0x13:
            mode = u8(exe, sections, cursor + 1)
            left_selector = u8(exe, sections, cursor + 2)
            right_immediate = u8(exe, sections, cursor + 3)
            target = u32(exe, sections, cursor + 4)
            rows.append({
                **base,
                "length": 8,
                "modeHex": hex8(mode),
                "condition": f"byte [object+0xa8 + 0x{left_selector:02x}] != {right_immediate}",
                "targetVa": target,
                "targetVaHex": hex32(target),
                "summary": f"if input-state byte +0x{left_selector:02x} != {right_immediate}, jump -> {hex32(target)}",
            })
            cursor += 8
            continue
        if opcode == 0x10:
            mode = u8(exe, sections, cursor + 1)
            dest_offset = u8(exe, sections, cursor + 2)
            value = u8(exe, sections, cursor + 3)
            rows.append({
                **base,
                "length": 4,
                "modeHex": hex8(mode),
                "operation": "set",
                "destination": f"byte [object+0xa8 + 0x{dest_offset:02x}]",
                "value": value,
                "valueHex": hex8(value),
                "summary": f"set input-state/latch byte +0x{dest_offset:02x} = {value}",
            })
            cursor += 4
            continue
        if opcode == 0x03:
            target = u32(exe, sections, cursor + 4)
            rows.append({
                **base,
                "length": 8,
                "targetVa": target,
                "targetVaHex": hex32(target),
                "summary": f"jump -> {hex32(target)}",
            })
            cursor = target
            if cursor == 0x00442CC5:
                rows[-1]["loopBackToAttachFrameScript"] = True
                break
            continue
        rows.append({
            **base,
            "length": 4,
            "summary": "unhandled/default command in this narrow decoder",
        })
        cursor += 4
    return rows


def scan_for_refs(exe: bytes, sections: list[dict], cns_strings: dict[int, str]) -> dict[str, Any]:
    payload = read_at(exe, sections, PAYLOAD_VA, 0x120)
    dword_values = [struct.unpack_from("<I", payload, offset)[0] for offset in range(0, len(payload) - 3, 4)]
    map_cns_refs = []
    all_cns_refs = []
    for offset in range(0, len(payload) - 3):
        value = struct.unpack_from("<I", payload, offset)[0]
        if value in cns_strings:
            row = {"offsetHex": f"0x{offset:02x}", "vaHex": hex32(value), "name": cns_strings[value]}
            all_cns_refs.append(row)
            if cns_strings[value].startswith("map"):
                map_cns_refs.append(row)
    return {
        "mapLoaderRefFound": MAP_LOADER_FUNCTION in dword_values,
        "mapCnsRefs": map_cns_refs,
        "allCnsRefs": all_cns_refs,
    }


def build_summary(exe_path: Path) -> dict[str, Any]:
    exe = exe_path.read_bytes()
    sections = read_sections(exe)
    cns_strings = find_cns_strings(exe, sections)
    payload_header = decode_payload_header(exe, sections)
    script_rows = decode_object_script(exe, sections)
    anim_script = decode_frame_script(exe, sections, ANIM_FRAME_SCRIPT_VA)
    idle_script = decode_frame_script(exe, sections, IDLE_FRAME_SCRIPT_VA)
    refs = scan_for_refs(exe, sections, cns_strings)
    initializer = next(row for row in script_rows if row["opcodeHex"] == "0x08")
    active_gate = next(
        (row for row in initializer["writes"] if row["field"] == "+0x14" and row["valueHex"] == "0x0101"),
        None,
    )
    input_base = any(row.get("opcodeHex") == "0x40" for row in script_rows)
    frame_script = any(row.get("opcodeHex") == "0x20" for row in script_rows)
    route_proof = False
    return {
        "title": "Object payload 0x442c75 decode",
        "summary": {
            "payloadVaHex": hex32(PAYLOAD_VA),
            "scriptCursorVaHex": hex32(SCRIPT_CURSOR_VA),
            "payloadObjectInitializerFound": active_gate is not None,
            "object14ActiveGateValueHex": active_gate["valueHex"] if active_gate else "",
            "inputStateBaseFound": input_base,
            "frameTimerScriptFound": frame_script,
            "frameTableVaHex": hex32(FRAME_TABLE_VA),
            "routeProofFound": route_proof,
            "mapLoaderRefFound": refs["mapLoaderRefFound"],
            "fieldMapTransitionPayloadFound": False,
            "sceneAutoTransitionClaim": False,
            "classification": "input-reactive active object/display payload, not a field-map transition payload",
        },
        "payloadHeader": payload_header,
        "scriptRows": script_rows,
        "frameScripts": [anim_script, idle_script],
        "referenceScan": refs,
        "derivedFlow": [
            "opcode 0x08 initializes object fields, including object +0x14 = 0x0101",
            "opcode 0x40 binds object +0xa8 to 0x0059e310, the input-state base",
            "opcode 0x20 attaches frame/timer script 0x00442cfd to object +0x64",
            "opcode 0x18 overwrites object +0x64 from table 0x00442cf5 using input-state byte +0x26",
            "opcode 0x5e mode 3 initializes helper/display position",
            "opcode 0x5e mode 2 runs object +0x64 frame script once",
            "opcode 0x01 stops the tick, so the script resumes on the next update",
            "opcode 0x13 loops while input-state byte +0x25 is not 1",
            "when input-state byte +0x25 is 1, opcode 0x10 clears that latch and opcode 0x03 jumps back to frame-script attachment",
        ],
        "nonClaims": [
            "This decode does not prove any concrete map route.",
            "This decode does not prove scene-driven automatic map movement.",
            "No map loader 0x0042449c reference appears in the decoded payload span.",
            "No map CNS resource pointer appears in the decoded payload span.",
            "The payload behaves like an active object/display/input loop, not a route table.",
        ],
        "nextCandidates": [
            "look for other object +0xec runtime values that are not the repeated 0x00442c75 payload",
            "trace active field object descriptors that receive +0xec before manual overlap",
            "only promote a route when the decoded script writes a map/root/coordinate target or reaches 0x0042449c",
        ],
    }


def markdown(summary: dict[str, Any]) -> str:
    s = summary["summary"]
    lines = [
        "# Object Payload 0x442c75 Decode",
        "",
        f"- payload 0x442c75: `{s['payloadVaHex']}`",
        f"- script cursor: `{s['scriptCursorVaHex']}`",
        f"- object initializer found: {s['payloadObjectInitializerFound']}",
        f"- object +0x14 = {s['object14ActiveGateValueHex']}",
        f"- input state base found: {s['inputStateBaseFound']}",
        f"- frame/timer script found: {s['frameTimerScriptFound']}",
        f"- route proof found: {s['routeProofFound']}",
        f"- map loader ref found: {s['mapLoaderRefFound']}",
        f"- field-map transition payload found: {s['fieldMapTransitionPayloadFound']}",
        f"- classification: {s['classification']}",
        "",
        "## Payload Header",
        "",
    ]
    header = summary["payloadHeader"]
    lines += [
        f"- resource open arg: `{header['resourceOpenArgVaHex']}`",
        f"- resource cleanup arg: `{header['resourceCleanupArgVaHex']}`",
        f"- actual script cursor: `{header['scriptCursorVaHex']}`",
        "",
        "## Script Flow",
        "",
        "| VA | opcode | meaning |",
        "|---|---|---|",
    ]
    for row in summary["scriptRows"]:
        lines.append(f"| `{row['vaHex']}` | `{row['opcodeHex']}` {row['opcodeName']} | {row['summary']} |")
    lines += [
        "",
        "## Frame Scripts",
        "",
    ]
    for frame in summary["frameScripts"]:
        lines.append(
            f"- `{frame['startVaHex']}` frames={frame['frameSequence']} gates={frame['gateSequence']} "
            f"loopsToSelf={frame['loopsToSelf']}"
        )
    lines += [
        "",
        "## Derived Flow",
        "",
    ]
    lines.extend(f"- {item}" for item in summary["derivedFlow"])
    lines += [
        "",
        "## 하지 않는 주장",
        "",
    ]
    lines.extend(f"- {item}" for item in summary["nonClaims"])
    lines += [
        "",
        "## 다음 후보",
        "",
    ]
    lines.extend(f"- {item}" for item in summary["nextCandidates"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict[str, Any]) -> str:
    s = summary["summary"]
    script_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['vaHex'])}</code></td>"
        f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
        f"<td>{html.escape(row['opcodeName'])}</td>"
        f"<td>{html.escape(row['summary'])}</td>"
        "</tr>"
        for row in summary["scriptRows"]
    )
    frame_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['startVaHex'])}</code></td>"
        f"<td>{html.escape(str(row['frameSequence']))}</td>"
        f"<td>{html.escape(str(row['gateSequence']))}</td>"
        f"<td>{row['loopsToSelf']}</td>"
        "</tr>"
        for row in summary["frameScripts"]
    )
    flow_items = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["derivedFlow"])
    non_claims = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["nonClaims"])
    next_items = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["nextCandidates"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="ko">',
        "<head>",
        '  <meta charset="utf-8" />',
        "  <title>Object Payload 0x442c75 Decode</title>",
        "  <style>",
        "    body{font-family:system-ui,-apple-system,Segoe UI,sans-serif;margin:24px;line-height:1.5;color:#1f2937;background:#f8fafc}",
        "    code{background:#e5e7eb;border-radius:4px;padding:1px 4px}",
        "    table{border-collapse:collapse;width:100%;background:white;margin:12px 0 24px}",
        "    th,td{border:1px solid #d1d5db;padding:8px;text-align:left;vertical-align:top}",
        "    th{background:#f3f4f6}",
        "    .marker{font-size:12px;color:#475569}",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Object Payload 0x442c75 Decode</h1>",
        f"  <p class=\"marker\">payload 0x442c75: {s['payloadVaHex']}</p>",
        f"  <p class=\"marker\">object initializer found: {s['payloadObjectInitializerFound']}</p>",
        f"  <p class=\"marker\">object +0x14 = {s['object14ActiveGateValueHex']}</p>",
        f"  <p class=\"marker\">input state base found: {s['inputStateBaseFound']}</p>",
        f"  <p class=\"marker\">frame/timer script found: {s['frameTimerScriptFound']}</p>",
        f"  <p class=\"marker\">route proof found: {s['routeProofFound']}</p>",
        f"  <p class=\"marker\">map loader ref found: {s['mapLoaderRefFound']}</p>",
        f"  <p class=\"marker\">field-map transition payload found: {s['fieldMapTransitionPayloadFound']}</p>",
        "  <p>This payload decodes as an input-reactive active object/display loop. It is not promoted to a map route.</p>",
        "  <script>",
        "    window.HWANSE_OBJECT_PAYLOAD_442C75_DECODE = {",
        f"      payloadObjectInitializerFound: {str(s['payloadObjectInitializerFound']).lower()},",
        f"      inputStateBaseFound: {str(s['inputStateBaseFound']).lower()},",
        f"      frameTimerScriptFound: {str(s['frameTimerScriptFound']).lower()},",
        f"      routeProofFound: {str(s['routeProofFound']).lower()},",
        f"      mapLoaderRefFound: {str(s['mapLoaderRefFound']).lower()},",
        f"      fieldMapTransitionPayloadFound: {str(s['fieldMapTransitionPayloadFound']).lower()}",
        "    };",
        "  </script>",
        "  <h2>Script Flow</h2>",
        "  <table><thead><tr><th>VA</th><th>opcode</th><th>name</th><th>meaning</th></tr></thead><tbody>",
        script_rows,
        "  </tbody></table>",
        "  <h2>Frame Scripts</h2>",
        "  <table><thead><tr><th>start</th><th>frames</th><th>gates</th><th>loops</th></tr></thead><tbody>",
        frame_rows,
        "  </tbody></table>",
        "  <h2>Derived Flow</h2>",
        f"  <ul>{flow_items}</ul>",
        "  <h2>하지 않는 주장</h2>",
        f"  <ul>{non_claims}</ul>",
        "  <h2>다음 후보</h2>",
        f"  <ul>{next_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict[str, Any], out_dir: Path = OUT, md_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "object_payload_442c75_decode.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    if md_out is not None:
        md_out.write_text(markdown(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=EXE)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--md-out", type=Path, help="Optional legacy markdown output path.")
    args = parser.parse_args()
    summary = build_summary(args.exe)
    write_outputs(summary, args.out_dir, args.md_out)
    print(f"wrote object payload decode -> {args.out_dir / 'object_payload_442c75_decode.json'}")


if __name__ == "__main__":
    main()
