#!/usr/bin/env python3
"""Summarize stream operands for object+0x61 consumer handlers."""
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 find_cns_strings, read_sections, va_to_offset


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

ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
FRONTIER_VALUES = {
    0x00542AE8: "frontier leaf pointer",
    0x00542B0C: "frontier reader",
    0x0053F46F: "frontier reader false branch target",
}

OBJECT61_GROUPS = [
    {
        "name": "collision/action mask test",
        "rangeStart": 0x0040D080,
        "rangeEnd": 0x0040D4D8,
        "routeImpact": "object action/collision tests; not a save-selector leaf chooser",
    },
    {
        "name": "object spawn/flag management",
        "rangeStart": 0x0040D4DD,
        "rangeEnd": 0x0040DDED,
        "routeImpact": "may branch through dword [stream+4]",
    },
    {
        "name": "status sync helper",
        "rangeStart": 0x0040DE28,
        "rangeEnd": 0x0040EF09,
        "routeImpact": "runtime object synchronization",
    },
]


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


def parse_hex(value: str | None) -> int | None:
    if not isinstance(value, str):
        return None
    return int(value, 16)


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


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 group_for_handler(handler_va: int) -> dict | None:
    for group in OBJECT61_GROUPS:
        if group["rangeStart"] <= handler_va <= group["rangeEnd"]:
            return group
    return None


def classify_operand(value: int | None, sections: list[dict], strings: dict[int, str]) -> dict:
    if value is None:
        return {"kind": "missing", "meaning": "-"}
    if value in FRONTIER_VALUES:
        return {"kind": "frontier-target", "meaning": FRONTIER_VALUES[value]}
    if value in strings:
        return {"kind": "cns-string", "meaning": strings[value]}
    if va_to_offset(sections, value) is not None:
        return {"kind": "executable-pointer", "meaning": "VA in executable image"}
    if value <= 0xFFFF:
        return {"kind": "small-scalar", "meaning": "small scalar or scene id"}
    return {"kind": "scalar", "meaning": "non-pointer scalar"}


def build_summary(exe: bytes, script_handler_table: dict) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    rows = []
    object_handler_opcodes = []
    for entry in script_handler_table.get("entries") or []:
        handler_va = parse_hex(entry.get("handlerVaHex"))
        if handler_va is None:
            continue
        group = group_for_handler(handler_va)
        if not group:
            continue
        object_handler_opcodes.append({
            "opcodeHex": entry.get("opcodeHex"),
            "handlerVaHex": entry.get("handlerVaHex"),
            "group": group["name"],
            "canJumpToDwordAtPlus4": bool((entry.get("streamEffect") or {}).get("canJumpToDwordAtPlus4")),
            "referenceCount": entry.get("referenceCount", 0),
        })
        for ref in entry.get("references") or []:
            if ref.get("source") != ROUTE_SOURCE or ref.get("target") != ROUTE_TARGET:
                continue
            word_va = parse_hex(ref.get("wordVaHex"))
            if word_va is None:
                continue
            value = dword_at(exe, sections, word_va)
            next_value = dword_at(exe, sections, word_va + 4)
            next_next_value = dword_at(exe, sections, word_va + 8)
            value_class = classify_operand(value, sections, strings)
            next_class = classify_operand(next_value, sections, strings)
            next_next_class = classify_operand(next_next_value, sections, strings)
            rows.append({
                "opcodeHex": entry.get("opcodeHex"),
                "handlerVaHex": entry.get("handlerVaHex"),
                "group": group["name"],
                "streamKind": ref.get("streamKind"),
                "streamVaHex": ref.get("streamVaHex"),
                "wordIndex": ref.get("wordIndex"),
                "wordVaHex": ref.get("wordVaHex"),
                "valueHex": hex32(value) if value is not None else None,
                "valueKind": value_class["kind"],
                "valueMeaning": value_class["meaning"],
                "nextDwordVaHex": hex32(word_va + 4),
                "nextDwordHex": hex32(next_value) if next_value is not None else None,
                "nextDwordKind": next_class["kind"],
                "nextDwordMeaning": next_class["meaning"],
                "nextNextDwordVaHex": hex32(word_va + 8),
                "nextNextDwordHex": hex32(next_next_value) if next_next_value is not None else None,
                "nextNextDwordKind": next_next_class["kind"],
                "nextNextDwordMeaning": next_next_class["meaning"],
                "canJumpToDwordAtPlus4": bool((entry.get("streamEffect") or {}).get("canJumpToDwordAtPlus4")),
                "cns": ref.get("cns"),
                "pointer": ref.get("pointer") is True,
                "routeImpact": group["routeImpact"],
            })

    branch_rows = [row for row in rows if row.get("canJumpToDwordAtPlus4")]
    direct_frontier_operand_rows = [
        row for row in rows
        if row.get("valueKind") == "frontier-target"
        or row.get("nextDwordKind") == "frontier-target"
        or row.get("nextNextDwordKind") == "frontier-target"
    ]
    branch_frontier_rows = [
        row for row in branch_rows
        if row.get("nextDwordKind") == "frontier-target"
    ]
    conclusion = (
        "Object+0x61 consumer handlers do appear in the map1_01a->map2_02d save-selector streams, "
        "but their stream operands do not point at the current frontier leaf, frontier reader, or false branch target. "
        "The only branch-capable object+0x61 row is opcode 0x2f at 0x00542e74; its dword [stream+4] is 0x00001618, "
        "a small scalar/scene-id value associated with map2_16j.cns, not a save-selector leaf pointer. "
        "This keeps the object+0x61 path blocked as route promotion evidence."
    )
    return {
        "scope": "object+0x61 consumer handlers resolved through current map1_01a->map2_02d save-selector stream operands",
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "objectHandlerOpcodeCount": len(object_handler_opcodes),
        "objectHandlerOpcodes": object_handler_opcodes,
        "routeOperandRowCount": len(rows),
        "branchCapableRowCount": len(branch_rows),
        "directFrontierOperandCount": len(direct_frontier_operand_rows),
        "branchFrontierOperandCount": len(branch_frontier_rows),
        "branchCapableRows": branch_rows,
        "directFrontierOperandRows": direct_frontier_operand_rows,
        "rows": rows,
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector object+0x61 Stream Operands",
        "",
        f"Scope: {summary['scope']}.",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- object handler opcodes: {summary['objectHandlerOpcodeCount']}",
        f"- route operand rows: {summary['routeOperandRowCount']}",
        f"- branch-capable rows: {summary['branchCapableRowCount']}",
        f"- direct frontier operands: {summary['directFrontierOperandCount']}",
        f"- branch frontier operands: {summary['branchFrontierOperandCount']}",
        f"- promotion status: {summary['promotionStatus']}",
        f"- conclusion: {summary['conclusion']}",
        "",
        "## Object Handler Opcodes",
        "",
        "| opcode | handler | group | jump [stream+4] | refs |",
        "| --- | --- | --- | --- | ---: |",
    ]
    for row in summary["objectHandlerOpcodes"]:
        lines.append(
            f"| `{row['opcodeHex']}` | `{row['handlerVaHex']}` | {row['group']} | "
            f"{row['canJumpToDwordAtPlus4']} | {row['referenceCount']} |"
        )
    lines.extend([
        "",
        "## Route Operand Rows",
        "",
        "| word | opcode | handler | stream | value | next dword | next kind | group | impact |",
        "| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["rows"]:
        lines.append(
            f"| `{row['wordVaHex']}` | `{row['opcodeHex']}` | `{row['handlerVaHex']}` | "
            f"`{row['streamKind']} {row['streamVaHex']}[{row['wordIndex']}]` | "
            f"`{row['valueHex']}` {row['valueMeaning']} | "
            f"`{row['nextDwordHex']}` {row['nextDwordMeaning']} | {row['nextDwordKind']} | "
            f"{row['group']} | {row['routeImpact']} |"
        )
    if not summary["rows"]:
        lines.append("| - | - | - | - | - | - | - | - | - |")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    opcode_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row['opcodeHex']))}</code></td>"
        f"<td><code>{html.escape(str(row['handlerVaHex']))}</code></td>"
        f"<td>{html.escape(str(row['group']))}</td>"
        f"<td>{row['canJumpToDwordAtPlus4']}</td>"
        f"<td>{row['referenceCount']}</td>"
        "</tr>"
        for row in summary["objectHandlerOpcodes"]
    )
    operand_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row['wordVaHex']))}</code></td>"
        f"<td><code>{html.escape(str(row['opcodeHex']))}</code></td>"
        f"<td><code>{html.escape(str(row['handlerVaHex']))}</code></td>"
        f"<td><code>{html.escape(str(row['streamKind']))} {html.escape(str(row['streamVaHex']))}[{row['wordIndex']}]</code></td>"
        f"<td><code>{html.escape(str(row['valueHex']))}</code><br>{html.escape(str(row['valueMeaning']))}</td>"
        f"<td><code>{html.escape(str(row['nextDwordHex']))}</code><br>{html.escape(str(row['nextDwordMeaning']))}</td>"
        f"<td>{html.escape(str(row['nextDwordKind']))}</td>"
        f"<td>{html.escape(str(row['group']))}</td>"
        f"<td>{html.escape(str(row['routeImpact']))}</td>"
        "</tr>"
        for row in summary["rows"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector object+0x61 Stream Operands</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1280px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector object+0x61 Stream Operands</h1>",
        f"<p>Scope: {html.escape(summary['scope'])}.</p>",
        "<ul>",
        f"<li>route: <code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code></li>",
        f"<li>object handler opcodes: {summary['objectHandlerOpcodeCount']}</li>",
        f"<li>route operand rows: {summary['routeOperandRowCount']}</li>",
        f"<li>branch-capable rows: {summary['branchCapableRowCount']}</li>",
        f"<li>direct frontier operands: {summary['directFrontierOperandCount']}</li>",
        f"<li>branch frontier operands: {summary['branchFrontierOperandCount']}</li>",
        f"<li>promotion status: {html.escape(summary['promotionStatus'])}</li>",
        "</ul>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Object Handler Opcodes</h2>",
        "<table><thead><tr><th>opcode</th><th>handler</th><th>group</th><th>jump [stream+4]</th><th>refs</th></tr></thead><tbody>",
        opcode_rows or '<tr><td colspan="5">No object handler opcodes.</td></tr>',
        "</tbody></table>",
        "<h2>Route Operand Rows</h2>",
        "<table><thead><tr><th>word</th><th>opcode</th><th>handler</th><th>stream</th><th>value</th><th>next dword</th><th>next kind</th><th>group</th><th>impact</th></tr></thead><tbody>",
        operand_rows or '<tr><td colspan="9">No route operand rows.</td></tr>',
        "</tbody></table>",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--script-handler-table", type=Path, default=OUT / "script_handler_table.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.script_handler_table, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote object+0x61 stream operands -> {args.out_dir / 'save_selector_object61_stream_operands.html'}")


if __name__ == "__main__":
    main()
