#!/usr/bin/env python3
"""Compare current save-selector opcodes with branch-state event handler opcodes."""
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
from summarize_script_handler_table import HANDLER_TABLE_VA, dword_at, section_name_for_va


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


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


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


def script_handler_for_opcode(exe: bytes, sections: list[dict], opcode: int) -> dict:
    entry_va = HANDLER_TABLE_VA + opcode * 4
    handler_va = dword_at(exe, sections, entry_va)
    return {
        "tableVaHex": hex32(HANDLER_TABLE_VA),
        "entryVaHex": hex32(entry_va),
        "handlerVaHex": hex32(handler_va) if handler_va is not None else None,
        "handlerSection": section_name_for_va(sections, handler_va) if handler_va is not None else None,
    }


def current_reader_va(selection_writers: dict) -> int | None:
    candidates = [
        parse_hex(row.get("vaHex"))
        for row in selection_writers.get("currentFrontierRows") or []
        if row.get("operation") == "reader" and row.get("selectionBufferOffsetHex") == "0x20"
    ]
    candidates = [value for value in candidates if value is not None]
    return min(candidates) if candidates else None


def matching_words(
    exe: bytes,
    leaf_streams: list[dict],
    dispatch_summary: dict,
    selection_writers: dict,
) -> list[dict]:
    sections = read_sections(exe)
    reader_va = current_reader_va(selection_writers)
    dispatch_by_opcode = {
        int((cluster.get("eventHandlerTable") or {}).get("opcodeHex"), 16): cluster
        for cluster in dispatch_summary.get("clusters") or []
        if (cluster.get("eventHandlerTable") or {}).get("opcodeHex")
    }
    rows = []
    seen_words = set()
    for stream in leaf_streams:
        if stream.get("source") != "map1_01a" or stream.get("target") != "map2_02d":
            continue
        for stream_kind, words_key in (("leaf", "words"), ("nested", "nestedWords")):
            for word in stream.get(words_key) or []:
                value = int(word["valueHex"], 16)
                opcode = value & 0xFF
                dispatch = dispatch_by_opcode.get(opcode)
                if not dispatch:
                    continue
                word_key = (word.get("vaHex"), word.get("valueHex"))
                if word_key in seen_words:
                    continue
                seen_words.add(word_key)
                va = parse_hex(word.get("vaHex"))
                save_handler = script_handler_for_opcode(exe, sections, opcode)
                rows.append({
                    "source": stream.get("source"),
                    "target": stream.get("target"),
                    "selector": stream.get("selector"),
                    "leafPointerHex": stream.get("leafPointerHex"),
                    "streamKind": stream_kind,
                    "wordIndex": word.get("index"),
                    "wordVaHex": word.get("vaHex"),
                    "valueHex": word.get("valueHex"),
                    "opcodeHex": f"0x{opcode:02x}",
                    "beforeCurrentFrontierReader": reader_va is not None and va is not None and va < reader_va,
                    "currentFrontierReaderHex": hex32(reader_va) if reader_va is not None else None,
                    "saveSelectorDispatch": save_handler,
                    "eventObjectDispatch": dispatch.get("eventHandlerTable") or {},
                    "eventObjectFunctionHex": dispatch.get("functionStartHex"),
                    "eventObjectCluster": dispatch.get("label"),
            "classification": (
                "overlap only: this word is in a save-selector leaf stream, so low byte dispatch uses 0x00440720 here; "
                "it is not evidence that the event/object handler table 0x0047f1d8 executed."
            ),
                })
    return rows


def build_summary(
    exe: bytes,
    leaf_streams: list[dict],
    dispatch_summary: dict,
    selection_writers: dict,
) -> dict:
    rows = matching_words(exe, leaf_streams, dispatch_summary, selection_writers)
    before_reader = [row for row in rows if row.get("beforeCurrentFrontierReader")]
    return {
        "scope": "current frontier opcode overlap between save-selector stream and branch-state event handlers",
        "currentFrontierReaderHex": rows[0]["currentFrontierReaderHex"] if rows else None,
        "overlapCount": len(rows),
        "overlapBeforeCurrentFrontierReaderCount": len(before_reader),
        "conclusion": (
            "The current save-selector leaf contains low-byte opcode values that match branch-state event/object handler indexes, "
            "but this stream is interpreted through save-selector handler table 0x00440720. "
            "The matching rows before 0x00542b0c are selectionBuffer setup/writer opcodes, not proof that the event/object "
            "branch-state writer handlers at 0x0047f1d8 ran."
        ),
        "rows": rows,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Branch State Opcode Overlap",
        "",
        "Low-byte opcode overlap between the current save-selector leaf stream and event/object branch-state writer handlers.",
        "",
        f"- current frontier reader: `{summary['currentFrontierReaderHex']}`",
        f"- overlaps: {summary['overlapCount']}",
        f"- overlaps before reader: {summary['overlapBeforeCurrentFrontierReaderCount']}",
        "",
        summary["conclusion"],
        "",
        "| word | value | opcode | before reader | save-selector handler | event/object handler | classification |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["rows"]:
        lines.append(
            f"| `{row['wordVaHex']}` | `{row['valueHex']}` | `{row['opcodeHex']}` | "
            f"{'yes' if row['beforeCurrentFrontierReader'] else 'no'} | "
            f"`{row['saveSelectorDispatch'].get('handlerVaHex')}` via `{row['saveSelectorDispatch'].get('tableVaHex')}` | "
            f"`{row['eventObjectFunctionHex']}` via `{row['eventObjectDispatch'].get('tableVaHex')}` | "
            f"{row['classification']} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = []
    for row in summary["rows"]:
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['wordVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['valueHex'])}</code></td>"
            f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
            f"<td>{'yes' if row['beforeCurrentFrontierReader'] else 'no'}</td>"
            f"<td><code>{html.escape(row['saveSelectorDispatch'].get('handlerVaHex') or '-')}</code><br>"
            f"via <code>{html.escape(row['saveSelectorDispatch'].get('tableVaHex') or '-')}</code></td>"
            f"<td><code>{html.escape(row.get('eventObjectFunctionHex') or '-')}</code><br>"
            f"via <code>{html.escape(row['eventObjectDispatch'].get('tableVaHex') or '-')}</code></td>"
            f"<td>{html.escape(row['classification'])}</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 Branch State Opcode Overlap</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 Branch State Opcode Overlap</h1>",
        f"  <p>Current frontier reader: <code>{html.escape(summary.get('currentFrontierReaderHex') or '-')}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <table><thead><tr><th>word</th><th>value</th><th>opcode</th><th>before reader</th><th>save-selector handler</th><th>event/object handler</th><th>classification</th></tr></thead>",
        f"  <tbody>{''.join(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_branch_state_opcode_overlap.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--leaf-streams", type=Path, default=OUT / "save_selector_leaf_streams.json")
    parser.add_argument("--dispatch", type=Path, default=OUT / "save_selector_branch_state_dispatch.json")
    parser.add_argument("--selection-writers", type=Path, default=OUT / "save_selector_selection_writers.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        json.loads(args.leaf_streams.read_text(encoding="utf-8")),
        json.loads(args.dispatch.read_text(encoding="utf-8")),
        json.loads(args.selection_writers.read_text(encoding="utf-8")),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote save selector branch state opcode overlap -> {args.out_dir / 'save_selector_branch_state_opcode_overlap.json'}")


if __name__ == "__main__":
    main()
