#!/usr/bin/env python3
"""Document the save-selector opcode 0x24 handler modes."""
from __future__ import annotations

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

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
HANDLER_VA = 0x0040C513
MODE_DISPATCH_VA = 0x0040C69D
MODE1_TARGET_VA = 0x0040C673
FINAL_ADVANCE_VA = 0x0040C6C0
OBJECT_STATE_TABLE_VA = 0x0040C64C
OBJECT_STATE_SELECTOR_VA = 0x0040C664


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def read_u32(exe: bytes, sections: list[dict], va: int) -> int:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        raise ValueError(f"unreadable VA 0x{va:08x}")
    return struct.unpack_from("<I", exe, offset)[0]


def read_byte(exe: bytes, sections: list[dict], va: int) -> int:
    offset = va_to_offset(sections, va)
    if offset is None or offset >= len(exe):
        raise ValueError(f"unreadable VA 0x{va:08x}")
    return exe[offset]


def boundary_mode(gate_paths: list[dict]) -> dict:
    for row in gate_paths:
        if row.get("source") == "map1_01a" and row.get("target") == "map2_02d":
            boundary = row.get("opcode24Boundary") or {}
            value = int(boundary.get("opcodeValueHex") or "0", 16)
            return {
                "source": row.get("source"),
                "target": row.get("target"),
                "opcodeVaHex": boundary.get("opcodeVaHex"),
                "opcodeValueHex": boundary.get("opcodeValueHex"),
                "streamPlus1": (value >> 8) & 0xFF,
                "streamPlus1Hex": f"0x{((value >> 8) & 0xFF):02x}",
                "streamPlus2": (value >> 16) & 0xFF,
                "streamPlus3": (value >> 24) & 0xFF,
            }
    return {}


def build_summary(exe: bytes, gate_paths: list[dict]) -> dict:
    sections = read_sections(exe)
    object_state_table = [read_u32(exe, sections, OBJECT_STATE_TABLE_VA + index * 4) for index in range(6)]
    object_state_selectors = [read_byte(exe, sections, OBJECT_STATE_SELECTOR_VA + index) for index in range(10)]
    object_state_rows = []
    for object_state, selector in enumerate(object_state_selectors, start=1):
        target = object_state_table[selector]
        if target == 0x0040C598:
            meaning = "write current object index 0x0059e33e to object+0x61 and set context+0x58=0"
        elif target == 0x0040C5B2:
            meaning = "scan runtime object table 0x0059db3c and write the first available slot+3 to object+0x61; set context+0x58=0"
        elif target == 0x0040C60A:
            meaning = "set context+0x58=1"
        elif target == 0x0040C619:
            meaning = "set context+0x58=2"
        elif target == 0x0040C66E:
            meaning = "no object/context update before final +4 advance"
        else:
            meaning = "unclassified object-state branch"
        object_state_rows.append({
            "objectState": object_state,
            "selector": selector,
            "targetVaHex": f"0x{target:08x}",
            "meaning": meaning,
        })
    mode_rows = [
        {
            "mode": 0,
            "targetVaHex": "0x0040c55c",
            "meaning": (
                "Call 0x0043329f for the current runtime object, then branch on object+0x58/object+0x67. "
                "This can set context+0x58 to 0/1/2/3 or update object+0x61."
            ),
        },
        {
            "mode": 1,
            "targetVaHex": "0x0040c673",
            "meaning": "Write byte(0x0059e348)+3 to current object+0x61, then advance stream +4.",
        },
        {
            "mode": 2,
            "targetVaHex": "0x0040c688",
            "meaning": "Write byte(0x0059e347) to current object+0x61, then advance stream +4.",
        },
    ]
    current = boundary_mode(gate_paths)
    current_mode = current.get("streamPlus1")
    current_mode_row = next((row for row in mode_rows if row["mode"] == current_mode), {})
    conclusion = (
        "The current map1_01a->map2_02d boundary uses opcode 0x24 mode 1. "
        "That mode updates the current runtime object's +0x61 field from global 0x0059e348 and then advances by +4; "
        "it does not by itself prove that the following pointer/table payload selects save-selector leaf 0x00542ae8. "
        "The next reverse-engineering step is to identify the producer/meaning of 0x0059e348 and the consumer of object+0x61/context+0x58."
    )
    return {
        "handlerVaHex": f"0x{HANDLER_VA:08x}",
        "modeDispatchVaHex": f"0x{MODE_DISPATCH_VA:08x}",
        "finalAdvanceVaHex": f"0x{FINAL_ADVANCE_VA:08x}",
        "globals": {
            "runtimeEnabledFlag": "0x0059e34d",
            "currentObjectIndex": "0x0059e33e",
            "runtimeObjectTable": "0x0059db30",
            "mode1SourceByte": "0x0059e348",
            "mode2SourceByte": "0x0059e347",
        },
        "currentBoundary": current,
        "currentMode": current_mode_row,
        "modes": mode_rows,
        "objectStateSwitch": {
            "selectorTableVaHex": f"0x{OBJECT_STATE_SELECTOR_VA:08x}",
            "targetTableVaHex": f"0x{OBJECT_STATE_TABLE_VA:08x}",
            "rows": object_state_rows,
        },
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    current = summary.get("currentBoundary") or {}
    current_mode = summary.get("currentMode") or {}
    lines = [
        "# Save Selector Opcode 0x24 Handler",
        "",
        f"Handler: `{summary.get('handlerVaHex')}`.",
        "",
        f"- current boundary: `{current.get('opcodeVaHex')}` value `{current.get('opcodeValueHex')}`",
        f"- current mode byte: `{current.get('streamPlus1Hex')}`",
        f"- current mode target: `{current_mode.get('targetVaHex')}`",
        f"- current mode meaning: {current_mode.get('meaning') or '-'}",
        f"- conclusion: {summary.get('conclusion')}",
        "",
        "## Globals",
        "",
    ]
    for name, value in (summary.get("globals") or {}).items():
        lines.append(f"- {name}: `{value}`")
    lines.extend([
        "",
        "## Mode Dispatch",
        "",
        "| mode | target | meaning |",
        "| ---: | --- | --- |",
    ])
    for row in summary.get("modes") or []:
        lines.append(f"| {row.get('mode')} | `{row.get('targetVaHex')}` | {row.get('meaning')} |")
    lines.extend([
        "",
        "## Object State Switch For Mode 0",
        "",
        "| object+0x67 state | selector | target | meaning |",
        "| ---: | ---: | --- | --- |",
    ])
    for row in ((summary.get("objectStateSwitch") or {}).get("rows") or []):
        lines.append(
            f"| {row.get('objectState')} | {row.get('selector')} | `{row.get('targetVaHex')}` | {row.get('meaning')} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    current = summary.get("currentBoundary") or {}
    current_mode = summary.get("currentMode") or {}
    mode_rows = "\n".join(
        "<tr>"
        f"<td>{row.get('mode')}</td>"
        f"<td><code>{html.escape(str(row.get('targetVaHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('meaning')))}</td>"
        "</tr>"
        for row in summary.get("modes") or []
    )
    state_rows = "\n".join(
        "<tr>"
        f"<td>{row.get('objectState')}</td>"
        f"<td>{row.get('selector')}</td>"
        f"<td><code>{html.escape(str(row.get('targetVaHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('meaning')))}</td>"
        "</tr>"
        for row in ((summary.get("objectStateSwitch") or {}).get("rows") or [])
    )
    global_items = "\n".join(
        f"<li>{html.escape(name)}: <code>{html.escape(str(value))}</code></li>"
        for name, value in (summary.get("globals") or {}).items()
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Opcode 0x24 Handler</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee}table{border-collapse:collapse}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Opcode 0x24 Handler</h1>",
        f"<p>Handler: <code>{html.escape(str(summary.get('handlerVaHex')))}</code>.</p>",
        "<ul>",
        f"<li>current boundary: <code>{html.escape(str(current.get('opcodeVaHex')))}</code> value <code>{html.escape(str(current.get('opcodeValueHex')))}</code></li>",
        f"<li>current mode byte: <code>{html.escape(str(current.get('streamPlus1Hex')))}</code></li>",
        f"<li>current mode target: <code>{html.escape(str(current_mode.get('targetVaHex')))}</code></li>",
        f"<li>current mode meaning: {html.escape(str(current_mode.get('meaning') or '-'))}</li>",
        f"<li>{html.escape(str(summary.get('conclusion')))}</li>",
        "</ul>",
        "<h2>Globals</h2><ul>",
        global_items,
        "</ul>",
        "<h2>Mode Dispatch</h2>",
        "<table><thead><tr><th>mode</th><th>target</th><th>meaning</th></tr></thead><tbody>",
        mode_rows,
        "</tbody></table>",
        "<h2>Object State Switch For Mode 0</h2>",
        "<table><thead><tr><th>object+0x67 state</th><th>selector</th><th>target</th><th>meaning</th></tr></thead><tbody>",
        state_rows,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode24_handler.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "save_selector_gate_paths.json", []),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote opcode 0x24 handler summary -> {args.out_dir / 'save_selector_opcode24_handler.json'}")


if __name__ == "__main__":
    main()
