#!/usr/bin/env python3
"""Summarize opcode 0x07 indexed selected-pointer rows in the current selector root."""
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
from summarize_script_handler_table import handler_for_opcode, section_name_for_va


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

ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
CURRENT_ROOT = 0x00540714
SCAN_END = 0x005429A4
LEAF_TABLE_START = 0x005429A8
ROOT_TABLE_POINTER = 0x005429DC
ROOT_TABLE_END_EXCLUSIVE = 0x00542A04
WRAPPER_ENTRY = 0x005429AC
WRAPPER_LEAF = 0x00542A04
FRONTIER_LEAF = 0x00542AE8
FRONTIER_READER = 0x00542B0C
OPCODE07_HANDLER = 0x0040AD9B

FRONTIER_TARGETS = {
    LEAF_TABLE_START: "leaf-table-window-start",
    ROOT_TABLE_POINTER: "root-table-pointer",
    WRAPPER_LEAF: "wrapper-leaf",
    FRONTIER_LEAF: "frontier-leaf",
    FRONTIER_READER: "frontier-reader",
}


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


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 classify_value(sections: list[dict], value: int | None) -> dict:
    if value is None:
        return {"kind": "unreadable"}
    section = section_name_for_va(sections, value)
    if value in FRONTIER_TARGETS:
        return {"kind": "frontier-target", "targetMeaning": FRONTIER_TARGETS[value]}
    if LEAF_TABLE_START <= value <= FRONTIER_READER:
        return {"kind": "leaf-table-window-value", "targetMeaning": "leaf-table-window-value"}
    if section:
        return {"kind": "pointer", "targetSection": section}
    if (value >> 16) <= 0x400 and (value & 0xFFFF) <= 0x10000:
        return {"kind": "script-scalar-or-pair"}
    return {"kind": "scalar"}


def build_summary(exe: bytes, leaf_table_context: dict | None = None) -> dict:
    sections = read_sections(exe)
    rows = []
    for va in range(CURRENT_ROOT, SCAN_END + 1, 4):
        value = dword_at(exe, sections, va)
        if value is None or (value & 0xFF) != 0x07:
            continue
        handler = handler_for_opcode(exe, sections, 0x07)
        index = (value >> 8) & 0xFF
        operand_table = dword_at(exe, sections, va + 4)
        selected_slot_va = operand_table + index * 4 if operand_table is not None else None
        selected_value = dword_at(exe, sections, selected_slot_va) if selected_slot_va is not None else None
        selected_class = classify_value(sections, selected_value)
        operand_class = classify_value(sections, operand_table)
        selected_slot_root_index = (
            (selected_slot_va - ROOT_TABLE_POINTER) // 4
            if selected_slot_va is not None
            and LEAF_TABLE_START <= selected_slot_va < ROOT_TABLE_END_EXCLUSIVE
            else None
        )
        selected_negative_entry_slot = (
            selected_slot_va is not None
            and LEAF_TABLE_START <= selected_slot_va < ROOT_TABLE_POINTER
        )
        selected_current_root_entry_slot = (
            selected_slot_va is not None
            and ROOT_TABLE_POINTER <= selected_slot_va < ROOT_TABLE_END_EXCLUSIVE
        )
        direct_frontier = bool(
            selected_value in FRONTIER_TARGETS
            or (isinstance(selected_value, int) and LEAF_TABLE_START <= selected_value <= FRONTIER_READER)
        )
        rows.append({
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            "handlerVaHex": handler.get("handlerVaHex"),
            "handlerSection": handler.get("handlerSection"),
            "index": index,
            "indexHex": f"0x{index:02x}",
            "operandTableHex": hex32(operand_table) if operand_table is not None else None,
            "operandTableKind": operand_class.get("kind"),
            "operandTableSection": operand_class.get("targetSection"),
            "selectedSlotVaHex": hex32(selected_slot_va) if selected_slot_va is not None else None,
            "selectedSlotRootRelativeIndex": selected_slot_root_index,
            "selectedNegativeRootEntrySlot": selected_negative_entry_slot,
            "selectedCurrentRootEntrySlot": selected_current_root_entry_slot,
            "selectedWrapperEntrySlot": selected_slot_va == WRAPPER_ENTRY,
            "selectedLeafTableWindowSlot": selected_slot_root_index is not None,
            "selectedValueHex": hex32(selected_value) if selected_value is not None else None,
            "selectedValueKind": selected_class.get("kind"),
            "selectedValueSection": selected_class.get("targetSection"),
            "selectedTargetMeaning": selected_class.get("targetMeaning"),
            "directFrontierTarget": direct_frontier,
            "validOpcode07Handler": handler.get("handlerVa") == OPCODE07_HANDLER,
        })
    direct_frontier_rows = [row for row in rows if row.get("directFrontierTarget")]
    leaf_table_slot_rows = [row for row in rows if row.get("selectedLeafTableWindowSlot")]
    negative_entry_slot_rows = [row for row in rows if row.get("selectedNegativeRootEntrySlot")]
    wrapper_entry_slot_rows = [row for row in rows if row.get("selectedWrapperEntrySlot")]
    current_root_entry_slot_rows = [row for row in rows if row.get("selectedCurrentRootEntrySlot")]
    valid_table_rows = [
        row for row in rows
        if row.get("operandTableKind") == "pointer" and row.get("selectedValueHex") is not None
    ]
    selector_context = leaf_table_context or {}
    conclusion = (
        "Opcode 0x07 is the selected-pointer indexer, and its handler zero-extends the stream index byte before "
        "loading [table + index*4], so it does not provide a signed negative-index path. The current 2:0 root's "
        "opcode 0x07 rows do not select any slot in the 0x005429a8..0x00542a00 leaf-table window, including the "
        "negative wrapper entry 0x005429ac. They also do not directly "
        "select the 0x005429a8 leaf table, root table pointer 0x005429dc, wrapper leaf 0x00542a04, frontier leaf "
        "0x00542ae8, or reader 0x00542b0c. Valid rows select script/action scalar dwords such as 0x0003e601 and "
        "0x0011e802, not frontier pointers. This closes the opcode 0x07 direct-selection path; promotion still needs "
        "the higher-level selector index/wrapper execution proof or a strict map1_01a hotspot."
    )
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "rootHex": hex32(CURRENT_ROOT),
        "scanRangeHex": f"{hex32(CURRENT_ROOT)}..{hex32(SCAN_END)}",
        "opcode07HandlerHex": hex32(OPCODE07_HANDLER),
        "opcode07IndexMode": "zero-extended-u8",
        "leafTableWindowStartHex": hex32(LEAF_TABLE_START),
        "leafTableWindowHex": f"{hex32(LEAF_TABLE_START)}..{hex32(ROOT_TABLE_END_EXCLUSIVE - 4)}",
        "rootTablePointerHex": hex32(ROOT_TABLE_POINTER),
        "wrapperEntryHex": hex32(WRAPPER_ENTRY),
        "wrapperLeafHex": hex32(WRAPPER_LEAF),
        "frontierLeafHex": hex32(FRONTIER_LEAF),
        "frontierReaderHex": hex32(FRONTIER_READER),
        "leafTableContextFrontierLeafRefHex": selector_context.get("frontierLeafRefVaHex"),
        "rowCount": len(rows),
        "validTableRowCount": len(valid_table_rows),
        "selectedLeafTableWindowSlotCount": len(leaf_table_slot_rows),
        "selectedNegativeRootEntrySlotCount": len(negative_entry_slot_rows),
        "selectedCurrentRootEntrySlotCount": len(current_root_entry_slot_rows),
        "selectedWrapperEntrySlotCount": len(wrapper_entry_slot_rows),
        "directFrontierTargetCount": len(direct_frontier_rows),
        "rows": rows,
        "directFrontierTargets": direct_frontier_rows,
        "leafTableSlotRows": leaf_table_slot_rows,
        "negativeRootEntrySlotRows": negative_entry_slot_rows,
        "wrapperEntrySlotRows": wrapper_entry_slot_rows,
        "promotionStatus": "blocked",
        "remainingProofs": [
            "decode the higher-level selector/table index that chooses the 2:0 leaf table entries",
            "prove wrapper 0x00542a04 executes into 0x00542ae8 in the normal route",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x07 Indexed Pointers",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- root: `{summary['rootHex']}`",
        f"- scan range: `{summary['scanRangeHex']}`",
        f"- opcode 0x07 handler: `{summary['opcode07HandlerHex']}`",
        f"- opcode 0x07 index mode: {summary['opcode07IndexMode']}",
        f"- row count: {summary['rowCount']}",
        f"- valid table row count: {summary['validTableRowCount']}",
        f"- selected leaf-table window slot count: {summary['selectedLeafTableWindowSlotCount']}",
        f"- selected negative root-entry slot count: {summary['selectedNegativeRootEntrySlotCount']}",
        f"- selected wrapper entry slot count: {summary['selectedWrapperEntrySlotCount']}",
        f"- direct frontier target count: {summary['directFrontierTargetCount']}",
        f"- leaf table window: `{summary['leafTableWindowHex']}`",
        f"- wrapper entry: `{summary['wrapperEntryHex']}`",
        f"- wrapper leaf: `{summary['wrapperLeafHex']}`",
        f"- frontier leaf: `{summary['frontierLeafHex']}`",
        f"- frontier reader: `{summary['frontierReaderHex']}`",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "| va | value | index | table | selected slot | slot index | selected value | selected kind | frontier? |",
        "| --- | --- | ---: | --- | --- | ---: | --- | --- | --- |",
    ]
    for row in summary["rows"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | {row['index']} | "
            f"`{row.get('operandTableHex') or '-'}` {row.get('operandTableKind') or ''} | "
            f"`{row.get('selectedSlotVaHex') or '-'}` | {row.get('selectedSlotRootRelativeIndex')} | "
            f"`{row.get('selectedValueHex') or '-'}` | "
            f"{row.get('selectedValueKind') or '-'} {row.get('selectedValueSection') or row.get('selectedTargetMeaning') or ''} | "
            f"{row.get('directFrontierTarget')} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    body = []
    for row in summary["rows"]:
        table = f"{row.get('operandTableHex') or '-'} {row.get('operandTableKind') or ''}"
        kind = f"{row.get('selectedValueKind') or '-'} {row.get('selectedValueSection') or row.get('selectedTargetMeaning') or ''}"
        body.append(
            "<tr>"
            f"<td><code>{html.escape(row['vaHex'])}</code></td>"
            f"<td><code>{html.escape(row['valueHex'])}</code></td>"
            f"<td>{row['index']}</td>"
            f"<td><code>{html.escape(table)}</code></td>"
            f"<td><code>{html.escape(row.get('selectedSlotVaHex') or '-')}</code></td>"
            f"<td>{html.escape(str(row.get('selectedSlotRootRelativeIndex')))}</td>"
            f"<td><code>{html.escape(row.get('selectedValueHex') or '-')}</code></td>"
            f"<td>{html.escape(kind)}</td>"
            f"<td>{row.get('directFrontierTarget')}</td>"
            "</tr>"
        )
    proof_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    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 Opcode 0x07 Indexed Pointers</title>",
        "  <style>",
        "    body { margin: 24px; background: #111; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin: 18px 0 28px; }",
        "    th, td { border: 1px solid #3a3a3a; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #202020; position: sticky; top: 0; }",
        "    code { color: #9bd4ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Opcode 0x07 Indexed Pointers</h1>",
        f"  <p>route: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; root <code>{summary['rootHex']}</code>; scan range <code>{summary['scanRangeHex']}</code>; opcode 0x07 handler <code>{summary['opcode07HandlerHex']}</code>.</p>",
        f"  <p>index mode: {html.escape(summary['opcode07IndexMode'])}; row count: {summary['rowCount']}; valid table row count: {summary['validTableRowCount']}; selected leaf-table window slot count: {summary['selectedLeafTableWindowSlotCount']}; selected negative root-entry slot count: {summary['selectedNegativeRootEntrySlotCount']}; selected wrapper entry slot count: {summary['selectedWrapperEntrySlotCount']}; direct frontier target count: {summary['directFrontierTargetCount']}; promotion status: {html.escape(summary['promotionStatus'])}</p>",
        f"  <p>leaf table window: <code>{summary['leafTableWindowHex']}</code>; wrapper entry <code>{summary['wrapperEntryHex']}</code>; wrapper leaf <code>{summary['wrapperLeafHex']}</code>; frontier leaf <code>{summary['frontierLeafHex']}</code>; frontier reader <code>{summary['frontierReaderHex']}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <table><thead><tr><th>va</th><th>value</th><th>index</th><th>table</th><th>selected slot</th><th>slot index</th><th>selected value</th><th>selected kind</th><th>frontier?</th></tr></thead>",
        f"  <tbody>{''.join(body)}</tbody></table>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode07_indexed_pointers.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.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("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "save_selector_leaf_table_context.json", {}),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote opcode 0x07 indexed pointers -> {args.out_dir / 'save_selector_opcode07_indexed_pointers.json'}")


if __name__ == "__main__":
    main()
