#!/usr/bin/env python3
"""Summarize save-selector selection buffer handlers."""
from __future__ import annotations

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

from probe_exe_scene_tables import read_sections, va_to_offset


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

HANDLERS = [
    {
        "opcodeHex": "0x10",
        "handlerVa": 0x0040B49E,
        "name": "build branch-state table",
        "summary": (
            "Chooses primary/secondary branch-state table from stream+1, passes stream+2 "
            "to helper 0x00410c90, then advances the script stream by 4 bytes."
        ),
        "evidence": [
            "reads stream+1 and stream+2",
            "references state tables 0x0059e370 and 0x0059e360",
            "calls helper 0x00410c90",
        ],
        "signatures": [
            "8a4002",
            "b860e35900",
            "b970e35900",
            "e8b9570000",
            "83404004",
        ],
    },
    {
        "opcodeHex": "0x11",
        "handlerVa": 0x0040B4E6,
        "name": "branch on selected state",
        "summary": (
            "Reads index (context+0xa8)[stream+2], then tests stateTable[index]. "
            "State value 1 falls through by 8 bytes; any other value jumps to dword [stream+4]."
        ),
        "evidence": [
            "reads context+0xa8 as a pointer",
            "reads stream+1 and stream+2",
            "writes context+0x40 from dword [stream+4] on branch",
        ],
        "signatures": [
            "8b80a8000000",
            "8a1401",
            "8a0c02",
            "83f901",
            "8b4004",
            "894140",
        ],
    },
    {
        "opcodeHex": "0x12",
        "handlerVa": 0x0040B55F,
        "name": "select previous/next active state slot",
        "summary": (
            "Scans the 12-slot branch-state table for a nonzero entry, starting from "
            "(context+0xa8)[stream+2] when runtime flag 0x00457744 is set, stores the "
            "selected slot back into (context+0xa8)[stream+2], then advances by 4 bytes."
        ),
        "evidence": [
            "uses 12-slot loop bounds",
            "reads and writes context+0xa8[stream+2]",
            "references runtime flag 0x00457744",
        ],
        "signatures": [
            "a044774500",
            "8b80a8000000",
            "8a0401",
            "88040a",
            "837df40c",
            "83404004",
        ],
    },
    {
        "opcodeHex": "0x13",
        "handlerVa": 0x0040B696,
        "name": "select matching runtime state slot",
        "summary": (
            "Computes the active slot base from 0x004576e9/0x00457750, clears "
            "(context+0xa8)[stream+2], scans 12 runtime slots, compares per-slot bytes "
            "from 0x0059e344 with slot fields +0x4a/+0x48, stores the matching slot index "
            "back into (context+0xa8)[stream+2], then advances by 4 bytes."
        ),
        "evidence": [
            "reads current runtime object index 0x0059e33e",
            "reads active order bytes 0x004576e9",
            "computes 0x00457750 + activeOrder[index] * 0xd8",
            "reads runtime comparison bytes from 0x0059e344",
            "writes the selected slot index to context+0xa8[stream+2]",
        ],
        "signatures": [
            "a03ee35900",
            "8a88e9764500",
            "0550774500",
            "8b89a8000000",
            "88040a",
            "837df80c",
            "a044e35900",
            "8a54084a",
            "8a5c0848",
            "83404004",
        ],
    },
    {
        "opcodeHex": "helper",
        "handlerVa": 0x00410C90,
        "name": "fill branch-state table",
        "summary": (
            "Dispatches by stream+2 value and fills a 12-byte state table. The simplest helper path "
            "writes 1 before the requested slot and 0 after it; later paths write 0/1/2 from party, "
            "menu, item, or progress-related runtime tables."
        ),
        "evidence": [
            "jump table has cases 0..11",
            "helper 0x00410de5 fills 12 slots with 1 then 0",
            "other helper paths write state values 0, 1, and 2",
        ],
        "signatures": [
            "ff2485b00d4100",
            "c6040801",
            "c6040800",
            "c6040802",
        ],
    },
]

HELPER_JUMP_TABLE = 0x00410DB0
HELPER_CASES = [
    {
        "case": 0,
        "caseVa": 0x00410CA6,
        "calleeVa": 0x00410DE5,
        "argument": "constant 2",
        "pattern": "prefix fill: slots before N become 1, remaining slots become 0",
        "stateValues": "1/0",
        "persistenceRisk": "supports predecessor fill 0x00000210; produces slots 0..1 as 1",
    },
    {
        "case": 1,
        "caseVa": 0x00410CB9,
        "calleeVa": 0x00410DE5,
        "argument": "constant 4",
        "pattern": "prefix fill: slots before N become 1, remaining slots become 0",
        "stateValues": "1/0",
        "persistenceRisk": "would overwrite table with slots 0..3 as 1",
    },
    {
        "case": 2,
        "caseVa": 0x00410CCC,
        "calleeVa": 0x00410DE5,
        "argument": "constant 5",
        "pattern": "prefix fill: slots before N become 1, remaining slots become 0",
        "stateValues": "1/0",
        "persistenceRisk": "would overwrite table with slots 0..4 as 1",
    },
    {
        "case": 3,
        "caseVa": 0x00410CDF,
        "calleeVa": 0x00410DE5,
        "argument": "constant 6",
        "pattern": "prefix fill: slots before N become 1, remaining slots become 0",
        "stateValues": "1/0",
        "persistenceRisk": "would overwrite table with slots 0..5 as 1",
    },
    {
        "case": 4,
        "caseVa": 0x00410CF2,
        "calleeVa": 0x00410DE5,
        "argument": "byte 0x004576e8",
        "pattern": "runtime prefix fill",
        "stateValues": "1/0",
        "persistenceRisk": "runtime value can overwrite the prefix width",
    },
    {
        "case": 5,
        "caseVa": 0x00410D09,
        "calleeVa": 0x00410E40,
        "argument": "constant 6",
        "pattern": "party/item threshold table; missing entries 0, insufficient entries 2, pass entries 1",
        "stateValues": "0/1/2",
        "persistenceRisk": "runtime party/item state can rewrite the first six slots",
    },
    {
        "case": 6,
        "caseVa": 0x00410D1C,
        "calleeVa": 0x00410F23,
        "argument": "constant 6",
        "pattern": "paired runtime flags at 0x004576ec/0x004576ed; writes 0/2/1",
        "stateValues": "0/1/2",
        "persistenceRisk": "runtime flags can rewrite the first six slots",
    },
    {
        "case": 7,
        "caseVa": 0x00410D2F,
        "calleeVa": 0x00410FC4,
        "argument": "constant 6",
        "pattern": "indexed table using 0x0059e33e, 0x0059e344, and 0x0045779a",
        "stateValues": "0/1",
        "persistenceRisk": "runtime table can rewrite the first six slots",
    },
    {
        "case": 8,
        "caseVa": 0x00410D42,
        "calleeVa": 0x00411068,
        "argument": "constant 5",
        "pattern": "counts nonzero flags at 0x004576f8 and fills a derived prefix width",
        "stateValues": "1/0",
        "persistenceRisk": "runtime count can rewrite the first five slots",
    },
    {
        "case": 9,
        "caseVa": 0x00410D55,
        "calleeVa": 0x00411138,
        "argument": "constant 6",
        "pattern": "runtime group from 0x0059e345 plus paired flags at 0x004576f8/0x004576f9",
        "stateValues": "0/1/2",
        "persistenceRisk": "runtime flags can rewrite the first six slots",
    },
    {
        "case": 10,
        "caseVa": 0x00410D68,
        "calleeVa": 0x004111E5,
        "argument": "byte 0x004576e8",
        "pattern": "compares object pointers from 0x0059db30 via helper 0x00433a30",
        "stateValues": "1/0",
        "persistenceRisk": "object comparison can rewrite a runtime-sized prefix",
    },
    {
        "case": 11,
        "caseVa": 0x00410D7F,
        "calleeVa": 0x0041127D,
        "argument": "word 0x0059db28",
        "pattern": "checks object table 0x0059db3c field +0x62 bit 0x80",
        "stateValues": "0/1",
        "persistenceRisk": "object flags can rewrite a runtime-sized prefix",
    },
]


def bytes_at(exe: bytes, sections: list[dict], va: int, size: int) -> bytes:
    offset = va_to_offset(sections, va)
    if offset is None:
        return b""
    return exe[offset: offset + size]


def dword_at(exe: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, offset)[0]


def helper_case_rows(exe: bytes, sections: list[dict]) -> list[dict]:
    rows = []
    for case in HELPER_CASES:
        entry_va = HELPER_JUMP_TABLE + case["case"] * 4
        actual = dword_at(exe, sections, entry_va)
        rows.append({
            **case,
            "caseHex": f"0x{case['case']:02x}",
            "jumpTableEntryVaHex": f"0x{entry_va:08x}",
            "caseVaHex": f"0x{case['caseVa']:08x}",
            "calleeVaHex": f"0x{case['calleeVa']:08x}",
            "actualCaseVaHex": f"0x{actual:08x}" if actual is not None else None,
            "jumpTableMatches": actual == case["caseVa"],
        })
    return rows


def build_summary(exe: bytes) -> dict:
    sections = read_sections(exe)
    rows = []
    for handler in HANDLERS:
        code = bytes_at(exe, sections, handler["handlerVa"], 0x420)
        checks = []
        for sig_hex in handler["signatures"]:
            sig = bytes.fromhex(sig_hex)
            checks.append({
                "signatureHex": sig_hex,
                "found": sig in code,
            })
        rows.append({
            **{key: value for key, value in handler.items() if key != "signatures"},
            "handlerVaHex": f"0x{handler['handlerVa']:08x}",
            "signatureChecks": checks,
            "allSignaturesFound": all(check["found"] for check in checks),
        })
    return {
        "scope": "save-selector script selection buffer and branch-state handlers",
        "selectionBuffer": {
            "contextFieldHex": "0x000000a8",
            "stateSlots": 12,
            "interpretation": (
                "Pointer to a per-script selection buffer addressed by byte offsets from stream+2. "
                "Opcodes 0x11, 0x12, and 0x13 use stream+2 as an offset into this buffer; the resulting "
                "byte selects one of 12 branch-state table slots."
            ),
        },
        "branchStateValues": {
            "0": "inactive/unavailable",
            "1": "active/pass branch test",
            "2": "visible but blocked or unmet condition",
        },
        "handlers": rows,
        "helperJumpTable": {
            "tableVaHex": f"0x{HELPER_JUMP_TABLE:08x}",
            "validCaseRangeHex": "0x00..0x0b",
            "cases": helper_case_rows(exe, sections),
        },
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Selection State",
        "",
        summary["scope"],
        "",
        (
            f"Selection buffer: context field `{summary['selectionBuffer']['contextFieldHex']}`, "
            f"values select one of {summary['selectionBuffer']['stateSlots']} branch-state slots."
        ),
        summary["selectionBuffer"]["interpretation"],
        "",
        "| opcode | handler | name | summary | signatures |",
        "| --- | --- | --- | --- | --- |",
    ]
    for row in summary["handlers"]:
        signatures = "ok" if row["allSignaturesFound"] else "missing"
        lines.append(
            f"| `{row['opcodeHex']}` | `{row['handlerVaHex']}` | {row['name']} | "
            f"{row['summary']} | {signatures} |"
        )
    lines.extend(["", "## State Values", ""])
    for value, label in summary["branchStateValues"].items():
        lines.append(f"- `{value}`: {label}")
    lines.extend([
        "",
        "## Helper Cases",
        "",
        f"Helper jump table `{summary['helperJumpTable']['tableVaHex']}` accepts cases `{summary['helperJumpTable']['validCaseRangeHex']}`.",
        "",
        "| case | entry | target | callee | argument | state values | pattern | persistence/reset note | verified |",
        "| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["helperJumpTable"]["cases"]:
        lines.append(
            f"| `{row['caseHex']}` | `{row['jumpTableEntryVaHex']}` | `{row['caseVaHex']}` | "
            f"`{row['calleeVaHex']}` | {row['argument']} | {row['stateValues']} | "
            f"{row['pattern']} | {row['persistenceRisk']} | {row['jumpTableMatches']} |"
        )
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    rows = []
    for row in summary["handlers"]:
        signatures = "ok" if row["allSignaturesFound"] else "missing"
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
            f"<td><code>{html.escape(row['handlerVaHex'])}</code></td>"
            f"<td>{html.escape(row['name'])}</td>"
            f"<td>{html.escape(row['summary'])}</td>"
            f"<td>{html.escape(signatures)}</td>"
            "</tr>"
        )
    values = "".join(
        f"<li><code>{html.escape(value)}</code>: {html.escape(label)}</li>"
        for value, label in summary["branchStateValues"].items()
    )
    helper_rows = []
    for row in summary["helperJumpTable"]["cases"]:
        helper_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['caseHex'])}</code></td>"
            f"<td><code>{html.escape(row['jumpTableEntryVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['caseVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['calleeVaHex'])}</code></td>"
            f"<td>{html.escape(row['argument'])}</td>"
            f"<td>{html.escape(row['stateValues'])}</td>"
            f"<td>{html.escape(row['pattern'])}</td>"
            f"<td>{html.escape(row['persistenceRisk'])}</td>"
            f"<td>{row['jumpTableMatches']}</td>"
            "</tr>"
        )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Selection State</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin-bottom: 24px; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; }",
        "    code { color: #f5d76e; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Selection State</h1>",
        f"  <p>{html.escape(summary['scope'])}</p>",
        f"  <p>Selection buffer: context field <code>{html.escape(summary['selectionBuffer']['contextFieldHex'])}</code>; values select one of {summary['selectionBuffer']['stateSlots']} branch-state slots.</p>",
        f"  <p>{html.escape(summary['selectionBuffer']['interpretation'])}</p>",
        "  <table><thead><tr><th>opcode</th><th>handler</th><th>name</th><th>summary</th><th>signatures</th></tr></thead>",
        f"  <tbody>{''.join(rows)}</tbody></table>",
        f"  <h2>State Values</h2><ul>{values}</ul>",
        f"  <h2>Helper Cases</h2><p>Helper jump table <code>{html.escape(summary['helperJumpTable']['tableVaHex'])}</code> accepts cases <code>{html.escape(summary['helperJumpTable']['validCaseRangeHex'])}</code>.</p>",
        "  <table><thead><tr><th>case</th><th>entry</th><th>target</th><th>callee</th><th>argument</th><th>state values</th><th>pattern</th><th>persistence/reset note</th><th>verified</th></tr></thead>",
        f"  <tbody>{''.join(helper_rows)}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    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())
    write_outputs(summary, args.out_dir)
    print(f"wrote save selector selection state -> {args.out_dir / 'save_selector_selection_state.html'}")


if __name__ == "__main__":
    main()
