#!/usr/bin/env python3
"""Check whether the current frontier branch reuses the slot selected by the nearest writer."""
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"
ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"


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


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


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


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


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 state_table(stream_plus_1: int) -> str:
    return "primaryBranchState" if stream_plus_1 == 0 else "secondaryBranchState"


def decode_selection_word(value: int, va: int) -> dict:
    opcode = value & 0xFF
    stream_plus_1 = (value >> 8) & 0xFF
    stream_plus_2 = (value >> 16) & 0xFF
    table = state_table(stream_plus_1)
    row = {
        "va": va,
        "vaHex": hex32(va),
        "value": value,
        "valueHex": hex32(value),
        "opcode": opcode,
        "opcodeHex": hex8(opcode),
        "streamPlus1Hex": hex8(stream_plus_1),
        "selectionBufferOffset": stream_plus_2,
        "selectionBufferOffsetHex": hex8(stream_plus_2),
        "stateTable": table,
    }
    if opcode == 0x10:
        row["operation"] = "fillBranchStateTable"
        row["helperArgument"] = stream_plus_2
        row["helperArgumentHex"] = hex8(stream_plus_2)
        row["validHelperDispatch"] = stream_plus_2 <= 0x0B
    elif opcode == 0x11:
        row["operation"] = "readSelectedStateAndBranch"
    elif opcode == 0x12:
        row["operation"] = "selectActiveStateSlot"
    elif opcode == 0x13:
        row["operation"] = "selectMatchingRuntimeSlot"
    else:
        row["operation"] = "other"
    return row


def scan_selection_words(exe: bytes, sections: list[dict], start_va: int, end_va: int) -> list[dict]:
    rows = []
    for va in range(start_va, end_va, 4):
        value = dword_at(exe, sections, va)
        if value is None:
            continue
        row = decode_selection_word(value, va)
        if row["opcode"] in {0x10, 0x11, 0x12, 0x13}:
            rows.append(row)
    return rows


def offset_hexes(rows: list[dict]) -> list[str]:
    return sorted({row["selectionBufferOffsetHex"] for row in rows})


def first_route(rows: list[dict]) -> dict:
    for row in rows:
        if row.get("source") == ROUTE_SOURCE and row.get("target") == ROUTE_TARGET:
            return row
    return {}


def summarize_between(
    exe: bytes,
    writer_chain_rows: list[dict],
    gate_path_rows: list[dict],
    secondary_reset_scope: dict,
) -> dict:
    sections = read_sections(exe)
    chain = first_route(writer_chain_rows)
    gate = first_route(gate_path_rows)
    nearest = chain.get("nearestLinearWriter") or {}
    writer_va = parse_hex(nearest.get("writerVaHex"))
    reader_va = parse_hex(chain.get("frontierReaderVaHex"))
    if writer_va is None or reader_va is None:
        raise ValueError("missing current writer/reader route rows")
    writer_value = dword_at(exe, sections, writer_va)
    reader_value = dword_at(exe, sections, reader_va)
    if writer_value is None or reader_value is None:
        raise ValueError("could not read writer/reader dwords")
    writer = decode_selection_word(writer_value, writer_va)
    reader = decode_selection_word(reader_value, reader_va)
    between_rows = scan_selection_words(exe, sections, writer_va, reader_va)
    after_writer_rows = [row for row in between_rows if row["va"] > writer_va]
    same_offset_writes_after = [
        row for row in after_writer_rows
        if row["opcode"] in {0x12, 0x13}
        and row["selectionBufferOffset"] == writer["selectionBufferOffset"]
    ]
    same_offset_reads_after = [
        row for row in after_writer_rows
        if row["opcode"] == 0x11
        and row["selectionBufferOffset"] == writer["selectionBufferOffset"]
    ]
    other_offset_writes_after = [
        row for row in after_writer_rows
        if row["opcode"] in {0x12, 0x13}
        and row["selectionBufferOffset"] != writer["selectionBufferOffset"]
    ]
    valid_secondary_fills_between = [
        row for row in after_writer_rows
        if row["opcode"] == 0x10
        and row["stateTable"] == "secondaryBranchState"
        and row.get("validHelperDispatch")
    ]
    invalid_secondary_fills_between = [
        row for row in after_writer_rows
        if row["opcode"] == 0x10
        and row["stateTable"] == "secondaryBranchState"
        and not row.get("validHelperDispatch")
    ]
    same_table_and_offset = (
        writer["opcode"] == 0x12
        and reader["opcode"] == 0x11
        and writer["stateTable"] == reader["stateTable"]
        and writer["selectionBufferOffset"] == reader["selectionBufferOffset"]
    )
    secondary_table = secondary_reset_scope.get("secondaryBranchState") or {}
    helper = secondary_reset_scope.get("helper") or {}
    no_direct_global_secondary_writer = secondary_reset_scope.get("noDirectGlobalSecondaryWriter") is True
    helper_only_opcode10 = helper.get("onlyDirectCallInsideOpcode10Handler") is True
    state_preserved = (
        same_table_and_offset
        and not same_offset_writes_after
        and not valid_secondary_fills_between
        and no_direct_global_secondary_writer
        and helper_only_opcode10
    )
    preservation_status = (
        "same-selection-slot-preserved-known-opcodes"
        if state_preserved
        else "same-selection-slot-preservation-open"
    )
    conclusion = (
        "The nearest writer and frontier reader use the same secondaryBranchState table and "
        "selectionBuffer[0x20]. Between 0x005428bc and 0x00542b0c, the scan finds no later "
        "selectionBuffer[0x20] writer/read and no valid secondaryBranchState opcode 0x10 fill. "
        "The later selection writes use other offsets and the intervening secondary fills use "
        "out-of-range helper arguments, so the known-opcode model preserves the selected slot. "
        "This still does not prove the selected state value is 1, does not prove the control path "
        "reaches the frontier reader, and does not provide a strict map1_01a source hotspot."
    )
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "nearestWriterVaHex": writer["vaHex"],
        "frontierReaderVaHex": reader["vaHex"],
        "writer": writer,
        "reader": reader,
        "sameTableAndOffset": same_table_and_offset,
        "sameTableName": writer["stateTable"] if same_table_and_offset else None,
        "sameSelectionBufferOffsetHex": writer["selectionBufferOffsetHex"] if same_table_and_offset else None,
        "scanRangeHex": f"{writer['vaHex']}..{reader['vaHex']}",
        "selectionOpcodeRowsBetweenCount": len(between_rows),
        "postWriterSameOffsetWriteCount": len(same_offset_writes_after),
        "postWriterSameOffsetReadCount": len(same_offset_reads_after),
        "postWriterOtherOffsetWriteCount": len(other_offset_writes_after),
        "postWriterOtherOffsetWriteOffsetsHex": offset_hexes(other_offset_writes_after),
        "validSecondaryFillBetweenCount": len(valid_secondary_fills_between),
        "invalidSecondaryFillBetweenCount": len(invalid_secondary_fills_between),
        "invalidSecondaryFillOffsetsHex": offset_hexes(invalid_secondary_fills_between),
        "sameOffsetWritesAfterWriter": same_offset_writes_after,
        "sameOffsetReadsAfterWriter": same_offset_reads_after,
        "otherOffsetWritesAfterWriter": other_offset_writes_after,
        "validSecondaryFillsBetween": valid_secondary_fills_between,
        "invalidSecondaryFillsBetween": invalid_secondary_fills_between,
        "selectionOpcodeRowsBetween": between_rows,
        "secondaryResetScope": {
            "secondaryRangeHex": secondary_table.get("rangeHex"),
            "noDirectGlobalSecondaryWriter": no_direct_global_secondary_writer,
            "helperOnlyCalledInsideOpcode10Handler": helper_only_opcode10,
            "globalResetRuledOut": secondary_reset_scope.get("globalResetRuledOut"),
        },
        "gateBoundary": {
            "dispatchStopVaHex": gate.get("dispatchStopVaHex"),
            "dispatchStopOpcodeHex": gate.get("dispatchStopOpcodeHex"),
            "dispatchStopHandlerVaHex": gate.get("dispatchStopHandlerVaHex"),
            "dispatchStopHandlerSection": gate.get("dispatchStopHandlerSection"),
            "dispatchStopIsCodeHandler": gate.get("dispatchStopIsCodeHandler"),
        },
        "knownOpcodeStatePreservationStatus": preservation_status,
        "statePreservedByKnownOpcodes": state_preserved,
        "branchStateValueStillRuntimeDependent": True,
        "controlPathStillUnproven": True,
        "strictHotspotStillMissing": True,
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    writer = summary["writer"]
    reader = summary["reader"]
    reset = summary["secondaryResetScope"]
    gate = summary["gateBoundary"]
    lines = [
        "# Save Selector Branch Gate Consistency",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- writer: `{summary['nearestWriterVaHex']}` `{writer['valueHex']}` {writer['operation']}",
        f"- reader: `{summary['frontierReaderVaHex']}` `{reader['valueHex']}` {reader['operation']}",
        f"- same table and offset: {summary['sameTableAndOffset']} ({summary['sameTableName']} `{summary['sameSelectionBufferOffsetHex']}`)",
        f"- later same-offset writes: {summary['postWriterSameOffsetWriteCount']}",
        f"- later same-offset reads before frontier reader: {summary['postWriterSameOffsetReadCount']}",
        f"- later other-offset writes: {summary['postWriterOtherOffsetWriteCount']} (`{', '.join(summary['postWriterOtherOffsetWriteOffsetsHex']) or '-'}`)",
        f"- valid secondary fills between: {summary['validSecondaryFillBetweenCount']}",
        f"- invalid secondary fills between: {summary['invalidSecondaryFillBetweenCount']} (`{', '.join(summary['invalidSecondaryFillOffsetsHex']) or '-'}`)",
        f"- no direct global secondary writer: {reset['noDirectGlobalSecondaryWriter']}",
        f"- helper only called inside opcode 0x10 handler: {reset['helperOnlyCalledInsideOpcode10Handler']}",
        f"- known-opcode preservation status: `{summary['knownOpcodeStatePreservationStatus']}`",
        f"- state preserved by known opcodes: {summary['statePreservedByKnownOpcodes']}",
        f"- branch state value still runtime-dependent: {summary['branchStateValueStillRuntimeDependent']}",
        f"- control path still unproven: {summary['controlPathStillUnproven']}",
        f"- strict hotspot still missing: {summary['strictHotspotStillMissing']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Boundary",
        "",
        f"- secondary table: `{reset['secondaryRangeHex']}`",
        f"- dispatch stop: `{gate['dispatchStopVaHex']}` opcode `{gate['dispatchStopOpcodeHex']}` handler `{gate['dispatchStopHandlerVaHex']}` in {gate['dispatchStopHandlerSection']}",
        "",
        "## Selection Opcode Rows Between Writer And Reader",
        "",
        "| va | value | op | operation | table | offset | helper valid? |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["selectionOpcodeRowsBetween"]:
        helper_valid = row.get("validHelperDispatch")
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | `{row['opcodeHex']}` | "
            f"{row['operation']} | {row['stateTable']} | `{row['selectionBufferOffsetHex']}` | "
            f"{helper_valid if helper_valid is not None else '-'} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    writer = summary["writer"]
    reader = summary["reader"]
    reset = summary["secondaryResetScope"]
    gate = summary["gateBoundary"]
    rows = []
    for row in summary["selectionOpcodeRowsBetween"]:
        helper_valid = row.get("validHelperDispatch")
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['vaHex'])}</code></td>"
            f"<td><code>{html.escape(row['valueHex'])}</code></td>"
            f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
            f"<td>{html.escape(row['operation'])}</td>"
            f"<td>{html.escape(row['stateTable'])}</td>"
            f"<td><code>{html.escape(row['selectionBufferOffsetHex'])}</code></td>"
            f"<td>{html.escape(str(helper_valid if helper_valid is not None else '-'))}</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 Gate Consistency</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    h1 { margin: 0 0 8px; font-size: 24px; }",
        "    h2 { margin: 24px 0 8px; font-size: 18px; }",
        "    p { max-width: 1120px; color: #bbb; line-height: 1.45; }",
        "    table { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { position: sticky; top: 0; background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Branch Gate Consistency</h1>",
        f"  <p>route <code>{html.escape(summary['source'])}</code> -&gt; <code>{html.escape(summary['target'])}</code>; "
        f"writer <code>{html.escape(summary['nearestWriterVaHex'])}</code> <code>{html.escape(writer['valueHex'])}</code>; "
        f"reader <code>{html.escape(summary['frontierReaderVaHex'])}</code> <code>{html.escape(reader['valueHex'])}</code>.</p>",
        f"  <p>same table and offset: {summary['sameTableAndOffset']} ({html.escape(str(summary['sameTableName']))} <code>{html.escape(str(summary['sameSelectionBufferOffsetHex']))}</code>); "
        f"later same-offset writes: {summary['postWriterSameOffsetWriteCount']}; "
        f"later same-offset reads before frontier reader: {summary['postWriterSameOffsetReadCount']}; "
        f"later other-offset writes: {summary['postWriterOtherOffsetWriteCount']} ({html.escape(', '.join(summary['postWriterOtherOffsetWriteOffsetsHex']) or '-')}); "
        f"valid secondary fills between: {summary['validSecondaryFillBetweenCount']}; "
        f"invalid secondary fills between: {summary['invalidSecondaryFillBetweenCount']} ({html.escape(', '.join(summary['invalidSecondaryFillOffsetsHex']) or '-')}); "
        f"known-opcode preservation status: <code>{html.escape(summary['knownOpcodeStatePreservationStatus'])}</code>; "
        f"state preserved by known opcodes: {summary['statePreservedByKnownOpcodes']}; "
        f"branch state value still runtime-dependent: {summary['branchStateValueStillRuntimeDependent']}; "
        f"strict hotspot still missing: {summary['strictHotspotStillMissing']}; "
        f"promotion status: {html.escape(summary['promotionStatus'])}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Boundary</h2>",
        f"  <p>secondary table <code>{html.escape(str(reset['secondaryRangeHex']))}</code>; no direct global secondary writer: {reset['noDirectGlobalSecondaryWriter']}; "
        f"helper only called inside opcode 0x10 handler: {reset['helperOnlyCalledInsideOpcode10Handler']}; "
        f"dispatch stop <code>{html.escape(str(gate['dispatchStopVaHex']))}</code> opcode <code>{html.escape(str(gate['dispatchStopOpcodeHex']))}</code> handler <code>{html.escape(str(gate['dispatchStopHandlerVaHex']))}</code> in {html.escape(str(gate['dispatchStopHandlerSection']))}.</p>",
        "  <h2>Selection Opcode Rows Between Writer And Reader</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>op</th><th>operation</th><th>table</th><th>offset</th><th>helper valid?</th></tr></thead><tbody>",
        *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_gate_consistency.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_branch_gate_consistency.html").write_text(html_page(summary), encoding="utf-8")


def build_summary(
    exe: bytes,
    writer_chain_rows: list[dict],
    gate_path_rows: list[dict],
    secondary_reset_scope: dict,
) -> dict:
    return summarize_between(exe, writer_chain_rows, gate_path_rows, secondary_reset_scope)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--writer-chain", type=Path, default=OUT / "save_selector_writer_chain.json")
    parser.add_argument("--gate-paths", type=Path, default=OUT / "save_selector_gate_paths.json")
    parser.add_argument("--secondary-reset-scope", type=Path, default=OUT / "save_selector_secondary_reset_scope.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.writer_chain, []),
        load_json(args.gate_paths, []),
        load_json(args.secondary_reset_scope, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote save selector branch gate consistency -> {args.out_dir / 'save_selector_branch_gate_consistency.html'}")


if __name__ == "__main__":
    main()
