#!/usr/bin/env python3
"""Summarize runtime writes to the savedat scene-selector byte globals."""
from __future__ import annotations

import argparse
import bisect
import html
import json
import struct
import sys
from collections import Counter
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset
from summarize_script_handler_table import DEFAULT_HANDLER_VA, analyze_stream_effect


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

RUNTIME_SELECTOR_GROUP_GLOBAL = 0x004576DA
RUNTIME_SELECTOR_SLOT_GLOBAL = 0x004576DB
RUNTIME_SELECTOR_HANDLER = 0x00406DBB
GENERAL_HANDLER_TABLE_VA = 0x00440538
SAVE_SELECTOR_HANDLER_TABLE_VA = 0x00440720
EVENT_OBJECT_HANDLER_TABLE_VA = 0x0047F1D8
RUNTIME_SELECTOR_OPCODE = 0x4F
MODE0_CONTINUATION_STREAM = 0x004DC518
MODE1_CONTINUATION_STREAM = 0x004DC514
SELECTED_ROOT_CONSUMER_OPCODE = 0x08
SELECTED_ROOT_CONSUMER_HANDLER = 0x0040ADC9
ROOT_PROLOGUE_PREVIOUS_WORD = 0x02780031
ROOT_PROLOGUE_NEXT_PREFIX = (0x0000006D, 0x00000005)
ROOT_PROLOGUE_COMMON_SIGNATURE = (
    ROOT_PROLOGUE_PREVIOUS_WORD,
    0x0000006D,
    0x00000005,
    0x0000016D,
)

SOURCE_SELECTOR = "0:0"
PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"
ADDRESS_PREDECESSOR_SELECTOR = "10:0"
SOURCE = "map1_01a"
TARGET = "map2_02d"
CURRENT_ROOT = 0x00540714


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 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 dword_window_around(
    exe: bytes,
    sections: list[dict],
    center_va: int,
    *,
    before: int = 1,
    after: int = 4,
) -> list[dict]:
    rows = []
    for relative in range(-before, after + 1):
        va = center_va + relative * 4
        value = dword_at(exe, sections, va)
        rows.append({
            "relativeWordOffset": relative,
            "relativeByteOffsetHex": f"{relative * 4:+#x}",
            "vaHex": hex32(va),
            "valueHex": hex32(value) if value is not None else None,
            "isCenterOpcode4f": relative == 0,
        })
    return rows


def prologue_pattern(exe: bytes, sections: list[dict], va: int) -> dict:
    previous = dword_at(exe, sections, va - 4)
    next0 = dword_at(exe, sections, va + 4)
    next1 = dword_at(exe, sections, va + 8)
    next2 = dword_at(exe, sections, va + 12)
    signature = (previous, next0, next1, next2)
    return {
        "previousHex": hex32(previous) if previous is not None else None,
        "nextPrefixHex": [
            hex32(next0) if next0 is not None else None,
            hex32(next1) if next1 is not None else None,
        ],
        "nextSignatureHex": [
            hex32(next0) if next0 is not None else None,
            hex32(next1) if next1 is not None else None,
            hex32(next2) if next2 is not None else None,
        ],
        "commonPreviousOpcode31": previous == ROOT_PROLOGUE_PREVIOUS_WORD,
        "commonNext6d05": (next0, next1) == ROOT_PROLOGUE_NEXT_PREFIX,
        "commonRouteSignature": signature == ROOT_PROLOGUE_COMMON_SIGNATURE,
    }


def section_name_for_va(sections: list[dict], va: int | None) -> str | None:
    if va is None:
        return None
    for section in sections:
        start = section["va"]
        end = start + section["raw_size"]
        if start <= va < end:
            return section["name"]
    return None


def section_by_name(sections: list[dict], name: str) -> dict:
    for section in sections:
        if section["name"] == name:
            return section
    raise ValueError(f"missing {name} section")


def selector_key(row: dict | None) -> str | None:
    if not row:
        return None
    return f"{row.get('group')}:{row.get('slot')}"


def selector_index(selectors: list[dict]) -> tuple[list[int], dict[int, dict]]:
    contexts: dict[int, dict] = {}
    for row in selectors:
        root = row.get("selectedPointer")
        if not isinstance(root, int):
            continue
        contexts[root] = row
    return sorted(contexts), contexts


def root_context_for_va(va: int, roots: list[int], contexts: dict[int, dict]) -> dict | None:
    index = bisect.bisect_right(roots, va) - 1
    if index < 0:
        return None
    root = roots[index]
    end = roots[index + 1] if index + 1 < len(roots) else root + 0x4000
    if va >= end:
        return None
    row = contexts[root]
    return {
        "selector": selector_key(row),
        "rootHex": row.get("selectedPointerHex") or hex32(root),
        "rootRangeHex": f"{hex32(root)}..{hex32(end)}",
        "relativeOffsetHex": f"+0x{va - root:03x}",
        "fieldMaps": row.get("fieldMaps") or [],
    }


def classify_moffs_ref(exe: bytes, ref_offset: int) -> dict:
    before = exe[max(0, ref_offset - 4): ref_offset]
    after = exe[ref_offset + 4: ref_offset + 10]
    opcode = exe[ref_offset - 1] if ref_offset > 0 else None
    kind = "unknown"
    access = "unknown"
    if opcode == 0xA0:
        kind = "movAlFromMoffs8"
        access = "read"
    elif opcode == 0xA2:
        kind = "movMoffs8FromAl"
        access = "write"
    elif opcode == 0xA1:
        kind = "movEaxFromMoffs"
        access = "read"
    elif opcode == 0xA3:
        kind = "movMoffsFromEax"
        access = "write"
    elif ref_offset >= 2 and exe[ref_offset - 2: ref_offset] == b"\x8a\x0d":
        kind = "movClFromMoffs8"
        access = "read"
    return {
        "instructionKind": kind,
        "access": access,
        "opcodeByteHex": hex8(opcode) if opcode is not None else None,
        "prefixBytesHex": before.hex(),
        "suffixBytesHex": after.hex(),
    }


def text_refs_to_value(exe: bytes, sections: list[dict], value: int) -> list[dict]:
    text = section_by_name(sections, ".text")
    raw_start = text["raw"]
    raw_end = raw_start + text["raw_size"]
    needle = struct.pack("<I", value)
    refs = []
    cursor = raw_start
    while True:
        hit = exe.find(needle, cursor, raw_end)
        if hit < 0:
            break
        ref_va = offset_to_va(sections, hit)
        if ref_va is not None:
            refs.append({
                "refVaHex": hex32(ref_va),
                "targetHex": hex32(value),
                **classify_moffs_ref(exe, hit),
            })
        cursor = hit + 1
    refs.sort(key=lambda row: row["refVaHex"])
    return refs


def table_names_for_ref(ref_va: int) -> list[dict]:
    tables = [
        ("general-object", GENERAL_HANDLER_TABLE_VA, 0x100),
        ("save-selector", SAVE_SELECTOR_HANDLER_TABLE_VA, 0x100),
        ("event-object", EVENT_OBJECT_HANDLER_TABLE_VA, 0x100),
    ]
    rows = []
    for name, base, count in tables:
        delta = ref_va - base
        if delta < 0 or delta % 4:
            continue
        opcode = delta // 4
        if 0 <= opcode < count:
            rows.append({
                "table": name,
                "tableVaHex": hex32(base),
                "opcode": opcode,
                "opcodeHex": hex8(opcode),
            })
    return rows


def handler_pointer_refs(exe: bytes, sections: list[dict], handler_va: int) -> list[dict]:
    needle = struct.pack("<I", handler_va)
    refs = []
    for section in sections:
        raw_start = section["raw"]
        raw_end = raw_start + section["raw_size"]
        cursor = raw_start
        while True:
            hit = exe.find(needle, cursor, raw_end)
            if hit < 0:
                break
            ref_va = offset_to_va(sections, hit)
            if ref_va is not None:
                refs.append({
                    "refVaHex": hex32(ref_va),
                    "section": section["name"],
                    "tableMatches": table_names_for_ref(ref_va),
                })
            cursor = hit + 1
    refs.sort(key=lambda row: row["refVaHex"])
    return refs


def handler_table_window(exe: bytes, sections: list[dict]) -> list[dict]:
    rows = []
    for table_name, base in [
        ("general-object", GENERAL_HANDLER_TABLE_VA),
        ("save-selector", SAVE_SELECTOR_HANDLER_TABLE_VA),
        ("event-object", EVENT_OBJECT_HANDLER_TABLE_VA),
    ]:
        for opcode in range(max(0, RUNTIME_SELECTOR_OPCODE - 2), RUNTIME_SELECTOR_OPCODE + 3):
            entry_va = base + opcode * 4
            handler = dword_at(exe, sections, entry_va)
            rows.append({
                "table": table_name,
                "tableVaHex": hex32(base),
                "opcode": opcode,
                "opcodeHex": hex8(opcode),
                "entryVaHex": hex32(entry_va),
                "handlerVaHex": hex32(handler) if handler is not None else None,
                "handlerSection": section_name_for_va(sections, handler),
                "isRuntimeSelectorHandler": handler == RUNTIME_SELECTOR_HANDLER,
            })
    return rows


def handler_entry_for_table(exe: bytes, sections: list[dict], table_va: int, opcode: int) -> dict:
    entry_va = table_va + opcode * 4
    handler_va = dword_at(exe, sections, entry_va)
    section = section_name_for_va(sections, handler_va) if handler_va is not None else None
    effect = {}
    if handler_va == DEFAULT_HANDLER_VA:
        effect = {"fixedAdvances": [{"bytes": 4}], "streamReads": [], "streamStores": []}
    elif handler_va is not None and section == ".text":
        effect = analyze_stream_effect(exe, sections, handler_va)
    advances = sorted({item.get("bytes") for item in effect.get("fixedAdvances", []) if item.get("bytes")})
    return {
        "opcode": opcode,
        "opcodeHex": hex8(opcode),
        "entryVaHex": hex32(entry_va),
        "handlerVa": handler_va,
        "handlerVaHex": hex32(handler_va) if handler_va is not None else None,
        "handlerSection": section,
        "isDefaultHandler": handler_va == DEFAULT_HANDLER_VA,
        "fixedAdvances": advances,
        "canJumpToDwordAtPlus4": effect.get("canJumpToDwordAtPlus4") is True,
        "writesStream": bool(effect.get("streamStores")),
        "matchesSelectedRootConsumer": opcode == SELECTED_ROOT_CONSUMER_OPCODE and handler_va == SELECTED_ROOT_CONSUMER_HANDLER,
    }


def trace_continuation_stream(
    exe: bytes,
    sections: list[dict],
    stream_va: int,
    *,
    table_va: int = SAVE_SELECTOR_HANDLER_TABLE_VA,
    max_steps: int = 8,
) -> list[dict]:
    rows = []
    va = stream_va
    seen: set[int] = set()
    for step in range(max_steps):
        if va in seen:
            rows.append({"step": step, "vaHex": hex32(va), "stopReason": "loop"})
            break
        seen.add(va)
        value = dword_at(exe, sections, va)
        if value is None:
            rows.append({"step": step, "vaHex": hex32(va), "stopReason": "unreadable"})
            break
        opcode = value & 0xFF
        handler = handler_entry_for_table(exe, sections, table_va, opcode)
        row = {
            "step": step,
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            **handler,
        }
        rows.append(row)
        advances = handler["fixedAdvances"]
        if len(advances) == 1 and not handler["canJumpToDwordAtPlus4"]:
            va += advances[0]
            continue
        if len(advances) == 1 and handler["canJumpToDwordAtPlus4"]:
            row["stopReason"] = "branch-or-fallthrough"
            break
        if not advances:
            row["stopReason"] = "no-fixed-advance"
            break
        row["stopReason"] = "multiple-advances"
        break
    return rows


def continuation_table_preview(exe: bytes, sections: list[dict], start_va: int, count: int = 48) -> list[dict]:
    rows = []
    for index in range(count):
        va = start_va + index * 4
        value = dword_at(exe, sections, va)
        if value is None:
            break
        section = section_name_for_va(sections, value)
        lo = value & 0xFFFF
        hi = (value >> 16) & 0xFFFF
        if section:
            classification = f"{section}-pointer"
        elif value <= 0xFFFF:
            classification = "small-scalar"
        elif hi <= 0x40 and lo <= 0x40:
            classification = "u16-pair-small"
        else:
            classification = "packed-scalar"
        rows.append(
            {
                "index": index,
                "vaHex": hex32(va),
                "valueHex": hex32(value),
                "lowByteOpcodeHex": hex8(value & 0xFF),
                "u16Lo": lo,
                "u16Hi": hi,
                "classification": classification,
                "pointsIntoSection": section,
            }
        )
    return rows


def continuation_stream_analysis(exe: bytes, sections: list[dict]) -> dict:
    mode1_trace = trace_continuation_stream(exe, sections, MODE1_CONTINUATION_STREAM)
    mode0_trace = trace_continuation_stream(exe, sections, MODE0_CONTINUATION_STREAM)
    mode1_first = mode1_trace[0] if mode1_trace else {}
    mode0_first = mode0_trace[0] if mode0_trace else {}
    return {
        "mode1ContinuationStreamHex": hex32(MODE1_CONTINUATION_STREAM),
        "mode0ContinuationStreamHex": hex32(MODE0_CONTINUATION_STREAM),
        "mode0IsMode1Plus4": MODE0_CONTINUATION_STREAM == MODE1_CONTINUATION_STREAM + 4,
        "handlerTableHex": hex32(SAVE_SELECTOR_HANDLER_TABLE_VA),
        "mode1FirstOpcodeHex": mode1_first.get("opcodeHex"),
        "mode1FirstHandlerHex": mode1_first.get("handlerVaHex"),
        "mode1StartsWithSelectedRootConsumer": mode1_first.get("matchesSelectedRootConsumer") is True,
        "mode0StartsAfterSelectedRootConsumer": mode0_first.get("matchesSelectedRootConsumer") is not True,
        "mode1Trace": mode1_trace,
        "mode0Trace": mode0_trace,
        "tablePreview": continuation_table_preview(exe, sections, MODE1_CONTINUATION_STREAM),
        "promotionImpact": (
            "mode1 continuation starts at save-selector opcode 0x08 selected-root consumer, "
            "so opcode 0x4f mode1 looks like selector-byte set followed by selected-root consumer entry. "
            "This grounds the continuation mechanism, but still does not prove who entered a root-local self-writer."
        ),
    }


def decode_runtime_selector_opcode(value: int) -> dict:
    mode = (value >> 8) & 0xFF
    group = (value >> 16) & 0xFF
    slot = (value >> 24) & 0xFF
    mode_meaning = {
        0: "push stream+4 and switch context stream to 0x004dc518",
        1: "write stream+2/+3 to runtime selector bytes and switch context stream to 0x004dc514",
        2: "call mode helper 0x00423319 with argument 0",
        3: "call mode helper 0x00423319 with argument 1",
    }.get(mode, "unknown mode")
    return {
        "opcodeHex": hex8(value & 0xFF),
        "mode": mode,
        "modeHex": hex8(mode),
        "group": group,
        "groupHex": hex8(group),
        "slot": slot,
        "slotHex": hex8(slot),
        "targetSelector": f"{group}:{slot}" if mode == 1 else None,
        "modeMeaning": mode_meaning,
    }


def scan_opcode4f_data_rows(
    exe: bytes,
    sections: list[dict],
    selectors: list[dict],
) -> tuple[list[dict], dict]:
    data = section_by_name(sections, ".data")
    roots, contexts = selector_index(selectors)
    rows = []
    for offset in range(data["raw"], data["raw"] + data["raw_size"] - 3, 4):
        value = struct.unpack_from("<I", exe, offset)[0]
        if (value & 0xFF) != RUNTIME_SELECTOR_OPCODE:
            continue
        va = data["va"] + offset - data["raw"]
        decoded = decode_runtime_selector_opcode(value)
        root_context = root_context_for_va(va, roots, contexts)
        row = {
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            **decoded,
            "rootContext": root_context,
        }
        if decoded.get("mode") == 1:
            row["prologuePattern"] = prologue_pattern(exe, sections, va)
            row["prologueWindow"] = dword_window_around(exe, sections, va)
        context_selector = (root_context or {}).get("selector")
        row["routeRole"] = route_role(context_selector, decoded.get("targetSelector"))
        row["isCurrentSelectorMode1Writer"] = (
            decoded.get("mode") == 1 and decoded.get("targetSelector") == CURRENT_SELECTOR
        )
        row["isCurrentSelfWriter"] = (
            context_selector == CURRENT_SELECTOR and row["isCurrentSelectorMode1Writer"]
        )
        rows.append(row)

    counts = Counter(row["modeHex"] for row in rows)
    mode1_rows = [row for row in rows if row["mode"] == 1]
    current_selector_rows = [row for row in mode1_rows if row.get("targetSelector") == CURRENT_SELECTOR]
    mode1_self_write_rows = [
        row for row in mode1_rows
        if ((row.get("rootContext") or {}).get("selector") == row.get("targetSelector"))
    ]
    mode1_cross_write_rows = [
        row for row in mode1_rows
        if ((row.get("rootContext") or {}).get("selector") != row.get("targetSelector"))
    ]
    route_context_rows = [
        row for row in mode1_rows
        if ((row.get("rootContext") or {}).get("selector") in {
            SOURCE_SELECTOR,
            PREDECESSOR_SELECTOR,
            CURRENT_SELECTOR,
            ADDRESS_PREDECESSOR_SELECTOR,
        })
    ]
    current_selector_rows_outside_current_root = [
        row for row in current_selector_rows
        if (row.get("rootContext") or {}).get("selector") != CURRENT_SELECTOR
    ]
    route_context_self_write_rows = [
        row for row in route_context_rows
        if ((row.get("rootContext") or {}).get("selector") == row.get("targetSelector"))
    ]
    route_context_common_signature_rows = [
        row for row in route_context_rows
        if (row.get("prologuePattern") or {}).get("commonRouteSignature") is True
    ]
    cross_writes_to_current_selector = [
        row for row in mode1_cross_write_rows
        if row.get("targetSelector") == CURRENT_SELECTOR
    ]
    mode1_rows_preceded_by_common_opcode31 = sum(
        1 for row in mode1_rows
        if (row.get("prologuePattern") or {}).get("commonPreviousOpcode31") is True
    )
    mode1_rows_with_common_next_6d05 = sum(
        1 for row in mode1_rows
        if (row.get("prologuePattern") or {}).get("commonNext6d05") is True
    )
    summary = {
        "opcode4fRowCount": len(rows),
        "modeCounts": dict(sorted(counts.items())),
        "mode1RowCount": len(mode1_rows),
        "mode1SelfWriteCount": len(mode1_self_write_rows),
        "mode1CrossWriteCount": len(mode1_cross_write_rows),
        "mode1RowsPrecededByCommonOpcode31Count": mode1_rows_preceded_by_common_opcode31,
        "mode1RowsWithCommonNext6d05Count": mode1_rows_with_common_next_6d05,
        "mode1CurrentSelectorWriterCount": len(current_selector_rows),
        "mode1CurrentSelectorWriterOutsideCurrentRootCount": len(current_selector_rows_outside_current_root),
        "routeContextMode1RowCount": len(route_context_rows),
        "routeContextSelfWriteCount": len(route_context_self_write_rows),
        "routeContextCommonSignatureCount": len(route_context_common_signature_rows),
        "crossWriteToCurrentSelectorCount": len(cross_writes_to_current_selector),
        "currentSelectorRows": current_selector_rows,
        "routeContextMode1Rows": route_context_rows,
        "rootSelfWritePattern": {
            "commonPreviousOpcode31Hex": hex32(ROOT_PROLOGUE_PREVIOUS_WORD),
            "commonNextPrefixHex": [hex32(value) for value in ROOT_PROLOGUE_NEXT_PREFIX],
            "commonRouteSignatureHex": [hex32(value) for value in ROOT_PROLOGUE_COMMON_SIGNATURE],
            "mode1SelfWriteCount": len(mode1_self_write_rows),
            "mode1CrossWriteCount": len(mode1_cross_write_rows),
            "mode1RowsPrecededByCommonOpcode31Count": mode1_rows_preceded_by_common_opcode31,
            "mode1RowsWithCommonNext6d05Count": mode1_rows_with_common_next_6d05,
            "routeContextSelfWriteCount": len(route_context_self_write_rows),
            "routeContextCommonSignatureCount": len(route_context_common_signature_rows),
            "crossWriteToCurrentSelectorCount": len(cross_writes_to_current_selector),
            "currentSelectorTargetOutsideCurrentRootCount": len(current_selector_rows_outside_current_root),
            "routeContextSelfWriteRows": route_context_self_write_rows,
            "crossWriteRows": mode1_cross_write_rows,
        },
    }
    return rows, summary


def route_role(context_selector: str | None, target_selector: str | None) -> str:
    if context_selector == CURRENT_SELECTOR and target_selector == CURRENT_SELECTOR:
        return "current-root-self-selector-write"
    if context_selector == SOURCE_SELECTOR and target_selector == SOURCE_SELECTOR:
        return "source-root-self-selector-write"
    if context_selector == PREDECESSOR_SELECTOR and target_selector == PREDECESSOR_SELECTOR:
        return "predecessor-root-self-selector-write"
    if context_selector == ADDRESS_PREDECESSOR_SELECTOR and target_selector == ADDRESS_PREDECESSOR_SELECTOR:
        return "address-predecessor-root-self-selector-write"
    if target_selector == CURRENT_SELECTOR:
        return "current-selector-target-write-outside-current-root"
    if context_selector in {SOURCE_SELECTOR, PREDECESSOR_SELECTOR, CURRENT_SELECTOR, ADDRESS_PREDECESSOR_SELECTOR}:
        return "route-context-other-selector-write"
    return "non-route-context"


def leaf_opcode_hits(leaf_streams: list[dict]) -> list[dict]:
    rows = []
    for leaf in leaf_streams:
        for stream_kind, key, stream_key in [
            ("leaf", "words", "leafPointerHex"),
            ("nested", "nestedWords", "nestedPointerHex"),
        ]:
            for word in leaf.get(key) or []:
                value_hex = word.get("valueHex")
                if not isinstance(value_hex, str):
                    continue
                value = int(value_hex, 16)
                if (value & 0xFF) != RUNTIME_SELECTOR_OPCODE:
                    continue
                rows.append({
                    "source": leaf.get("source"),
                    "target": leaf.get("target"),
                    "selector": leaf.get("selector"),
                    "streamKind": stream_kind,
                    "streamVaHex": leaf.get(stream_key),
                    "wordIndex": word.get("index"),
                    "wordVaHex": word.get("vaHex"),
                    "valueHex": value_hex,
                    **decode_runtime_selector_opcode(value),
                })
    return rows


def build_summary(exe: bytes, selectors: list[dict], leaf_streams: list[dict]) -> dict:
    sections = read_sections(exe)
    group_refs = text_refs_to_value(exe, sections, RUNTIME_SELECTOR_GROUP_GLOBAL)
    slot_refs = text_refs_to_value(exe, sections, RUNTIME_SELECTOR_SLOT_GLOBAL)
    handler_refs = handler_pointer_refs(exe, sections, RUNTIME_SELECTOR_HANDLER)
    opcode_rows, opcode_summary = scan_opcode4f_data_rows(exe, sections, selectors)
    continuation_analysis = continuation_stream_analysis(exe, sections)
    leaf_hits = leaf_opcode_hits(leaf_streams)
    writer_ref_pairs = [
        row for row in group_refs + slot_refs
        if row.get("access") == "write" and "0x00406e" in row.get("refVaHex", "")
    ]
    current_rows = opcode_summary["currentSelectorRows"]
    current_self_rows = [
        row for row in current_rows
        if row.get("isCurrentSelfWriter")
    ]
    source_or_predecessor_current_writers = [
        row for row in current_rows
        if (row.get("rootContext") or {}).get("selector") in {SOURCE_SELECTOR, PREDECESSOR_SELECTOR}
    ]
    root_pattern = opcode_summary["rootSelfWritePattern"]
    conclusion = (
        "The runtime selector byte globals are written by general-object opcode 0x4f mode 1. "
        "Static data contains exactly one mode-1 write to selector 2:0, but it lives inside the "
        "current selector 2:0 root itself. The source root 0:0 and predecessor root 1:0 only contain "
        "self-selector writes, and all four route-context mode-1 rows share the selector-root prologue "
        "signature. No cross-root write to selector 2:0 is present in the source/predecessor roots or "
        "the route frontier leaf streams. This documents the selector-byte write mechanism and the root "
        "self-identification pattern, but does not prove normal execution order for map1_01a -> map2_02d."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "currentSelector": CURRENT_SELECTOR,
        "currentRootHex": hex32(CURRENT_ROOT),
        "runtimeSelectorGlobals": {
            "groupGlobalHex": hex32(RUNTIME_SELECTOR_GROUP_GLOBAL),
            "slotGlobalHex": hex32(RUNTIME_SELECTOR_SLOT_GLOBAL),
            "groupTextRefCount": len(group_refs),
            "slotTextRefCount": len(slot_refs),
            "groupRefs": group_refs,
            "slotRefs": slot_refs,
        },
        "runtimeSelectorHandler": {
            "handlerVaHex": hex32(RUNTIME_SELECTOR_HANDLER),
            "generalHandlerTableHex": hex32(GENERAL_HANDLER_TABLE_VA),
            "saveSelectorHandlerTableHex": hex32(SAVE_SELECTOR_HANDLER_TABLE_VA),
            "eventObjectHandlerTableHex": hex32(EVENT_OBJECT_HANDLER_TABLE_VA),
            "opcodeHex": hex8(RUNTIME_SELECTOR_OPCODE),
            "handlerPointerRefCount": len(handler_refs),
            "handlerPointerRefs": handler_refs,
            "handlerTableWindow": handler_table_window(exe, sections),
            "mode1Semantics": (
                "mode 1 reads stream+2 as selector group and stream+3 as selector slot, "
                "writes them to 0x004576da/0x004576db, pushes stream+4, and switches context stream to 0x004dc514"
            ),
        },
        "continuationStreamAnalysis": continuation_analysis,
        "opcode4fDataScan": {
            **opcode_summary,
            "rows": opcode_rows,
        },
        "leafStreamOpcode4fHitCount": len(leaf_hits),
        "leafStreamOpcode4fHits": leaf_hits,
        "writerPairNearHandlerCount": len(writer_ref_pairs),
        "mode1CurrentSelfWriterCount": len(current_self_rows),
        "sourceOrPredecessorCurrentSelectorWriterCount": len(source_or_predecessor_current_writers),
        "rootSelfWritePatternPromotesRoute": False,
        "rootSelfWritePatternConclusion": (
            f"mode1 self writes={root_pattern['mode1SelfWriteCount']}, "
            f"cross writes={root_pattern['mode1CrossWriteCount']}, "
            f"route common signature rows={root_pattern['routeContextCommonSignatureCount']}, "
            f"cross writes to selector 2:0={root_pattern['crossWriteToCurrentSelectorCount']}; "
            "current selector 2:0 matches the same root self-write prologue pattern as the confirmed "
            "source/predecessor/address-predecessor selector roots."
        ),
        "selectorByteWriteMechanismIdentified": (
            len(writer_ref_pairs) == 2
            and any(
                any(match.get("table") == "general-object" and match.get("opcodeHex") == "0x4f"
                    for match in ref.get("tableMatches") or [])
                for ref in handler_refs
            )
        ),
        "currentSelectorByteWriterFound": len(current_rows) == 1,
        "currentSelectorWriterIsCurrentRootSelfWrite": len(current_self_rows) == 1,
        "executionOrderProven": False,
        "selectorByteWritePromotesRoute": False,
        "promotionStatus": "blocked",
        "remainingProofs": [
            "prove how the runtime enters selector root 2:0 from the confirmed map1_01a route",
            "find a source/predecessor-root opcode 0x4f mode1 write to selector 2:0, if one exists in an unscanned stream",
            "capture a real savedat/runtime trace with selector bytes 2:0 and selected pointer 0x00540714",
            "find a strict map1_01a source hotspot or coordinate table for map2_02d",
        ],
        "conclusion": conclusion,
    }


def row_selector_text(row: dict) -> str:
    context = row.get("rootContext") or {}
    return (
        f"{context.get('selector') or '-'} {context.get('rootHex') or '-'} "
        f"{context.get('relativeOffsetHex') or ''}"
    ).strip()


def markdown(summary: dict) -> str:
    globals_ = summary["runtimeSelectorGlobals"]
    handler = summary["runtimeSelectorHandler"]
    continuation = summary.get("continuationStreamAnalysis") or {}
    scan = summary["opcode4fDataScan"]
    root_pattern = scan["rootSelfWritePattern"]
    lines = [
        "# Save Selector Runtime Selector Byte Writes",
        "",
        f"- route: `{summary['source']}` -> `{summary['target']}`",
        f"- current selector/root: `{summary['currentSelector']}` `{summary['currentRootHex']}`",
        f"- selector globals: group `{globals_['groupGlobalHex']}`, slot `{globals_['slotGlobalHex']}`",
        f"- group/slot text refs: {globals_['groupTextRefCount']} / {globals_['slotTextRefCount']}",
        f"- runtime selector handler: `{handler['handlerVaHex']}`",
        f"- handler table opcode: general `{handler['generalHandlerTableHex']}` opcode `{handler['opcodeHex']}`",
        f"- writer pair near handler: {summary['writerPairNearHandlerCount']}",
        f"- opcode 0x4f data rows: {scan['opcode4fRowCount']}",
        f"- mode 1 rows: {scan['mode1RowCount']}",
        f"- mode 1 self-write rows: {root_pattern['mode1SelfWriteCount']}",
        f"- mode 1 cross-write rows: {root_pattern['mode1CrossWriteCount']}",
        f"- mode 1 rows preceded by `{root_pattern['commonPreviousOpcode31Hex']}`: {root_pattern['mode1RowsPrecededByCommonOpcode31Count']}",
        f"- mode 1 rows with next prefix `{', '.join(root_pattern['commonNextPrefixHex'])}`: {root_pattern['mode1RowsWithCommonNext6d05Count']}",
        f"- mode 1 current-selector writers: {scan['mode1CurrentSelectorWriterCount']}",
        f"- mode 1 current-selector writers outside current root: {scan['mode1CurrentSelectorWriterOutsideCurrentRootCount']}",
        f"- route-context self-write rows: {root_pattern['routeContextSelfWriteCount']}",
        f"- route-context common prologue signature rows: {root_pattern['routeContextCommonSignatureCount']}",
        f"- cross writes to selector `2:0`: {root_pattern['crossWriteToCurrentSelectorCount']}",
        f"- source/predecessor current-selector writers: {summary['sourceOrPredecessorCurrentSelectorWriterCount']}",
        f"- leaf-stream opcode 0x4f hits: {summary['leafStreamOpcode4fHitCount']}",
        f"- mode1 continuation stream: `{continuation.get('mode1ContinuationStreamHex')}`",
        f"- mode1 starts with selected-root consumer: {continuation.get('mode1StartsWithSelectedRootConsumer')}",
        f"- mode0 continuation stream: `{continuation.get('mode0ContinuationStreamHex')}`",
        f"- mode0 is mode1+4: {continuation.get('mode0IsMode1Plus4')}",
        f"- root self-write pattern promotes route: {summary['rootSelfWritePatternPromotesRoute']}",
        f"- selector byte write promotes route: {summary['selectorByteWritePromotesRoute']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        summary["rootSelfWritePatternConclusion"],
        "",
        "## Continuation Stream Preview",
        "",
        continuation.get("promotionImpact") or "",
        "",
        "| mode | stream | first opcode | first handler | selected-root consumer | trace stop |",
        "| --- | --- | --- | --- | --- | --- |",
    ]
    for mode_key in ["mode1", "mode0"]:
        trace = continuation.get(f"{mode_key}Trace") or []
        first = trace[0] if trace else {}
        stop = next((row.get("stopReason") for row in trace if row.get("stopReason")), "-")
        lines.append(
            f"| `{mode_key}` | `{continuation.get(f'{mode_key}ContinuationStreamHex')}` | "
            f"`{first.get('opcodeHex') or '-'}` | `{first.get('handlerVaHex') or '-'}` | "
            f"`{str(first.get('matchesSelectedRootConsumer') is True).lower()}` | {stop or '-'} |"
        )
    lines.extend([
        "",
        "### Continuation trace",
        "",
        "| mode | step | va | value | opcode | handler | advance | stop |",
        "| --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for mode_key in ["mode1", "mode0"]:
        for row in continuation.get(f"{mode_key}Trace") or []:
            advances = "/".join(f"+{value}" for value in row.get("fixedAdvances", [])) or "-"
            lines.append(
                f"| `{mode_key}` | {row.get('step')} | `{row.get('vaHex')}` | `{row.get('valueHex', '-')}` | "
                f"`{row.get('opcodeHex', '-')}` | `{row.get('handlerVaHex') or '-'}` | {advances} | {row.get('stopReason') or '-'} |"
            )
    lines.extend([
        "",
        "### Continuation table preview",
        "",
        "| index | va | value | low opcode | u16 lo | u16 hi | class |",
        "| ---: | --- | --- | --- | ---: | ---: | --- |",
    ])
    for row in continuation.get("tablePreview") or []:
        lines.append(
            f"| {row['index']} | `{row['vaHex']}` | `{row['valueHex']}` | `{row['lowByteOpcodeHex']}` | "
            f"{row['u16Lo']} | {row['u16Hi']} | {row['classification']} |"
        )
    lines.extend([
        "",
        "## Handler References",
        "",
        "| ref | section | table match |",
        "| --- | --- | --- |",
    ])
    for row in handler["handlerPointerRefs"]:
        matches = ", ".join(
            f"{item['table']}:{item['opcodeHex']}" for item in row.get("tableMatches") or []
        ) or "-"
        lines.append(f"| `{row['refVaHex']}` | {row['section']} | {matches} |")
    if not handler["handlerPointerRefs"]:
        lines.append("| - | - | - |")

    lines.extend([
        "",
        "## Selector Global Text Refs",
        "",
        "| target | ref | access | instruction |",
        "| --- | --- | --- | --- |",
    ])
    for row in globals_["groupRefs"] + globals_["slotRefs"]:
        lines.append(
            f"| `{row['targetHex']}` | `{row['refVaHex']}` | {row['access']} | {row['instructionKind']} |"
        )

    lines.extend([
        "",
        "## Route Mode 1 Rows",
        "",
        "| va | value | target selector | root context | prologue | role |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in scan["routeContextMode1Rows"]:
        pattern = row.get("prologuePattern") or {}
        prologue = (
            f"prev={pattern.get('previousHex') or '-'}; "
            f"next={', '.join(pattern.get('nextSignatureHex') or []) or '-'}; "
            f"common={pattern.get('commonRouteSignature')}"
        )
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | `{row.get('targetSelector') or '-'}` | "
            f"{row_selector_text(row)} | {prologue} | {row['routeRole']} |"
        )
    if not scan["routeContextMode1Rows"]:
        lines.append("| - | - | - | - | - | - |")

    lines.extend([
        "",
        "## Cross Writes",
        "",
        "| va | value | target selector | root context | role |",
        "| --- | --- | --- | --- | --- |",
    ])
    for row in root_pattern["crossWriteRows"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | `{row.get('targetSelector') or '-'}` | "
            f"{row_selector_text(row)} | {row['routeRole']} |"
        )
    if not root_pattern["crossWriteRows"]:
        lines.append("| - | - | - | - | - |")

    lines.extend([
        "",
        "## Remaining Proofs",
        "",
    ])
    for item in summary["remainingProofs"]:
        lines.append(f"- {item}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    globals_ = summary["runtimeSelectorGlobals"]
    handler = summary["runtimeSelectorHandler"]
    continuation = summary.get("continuationStreamAnalysis") or {}
    scan = summary["opcode4fDataScan"]
    root_pattern = scan["rootSelfWritePattern"]

    handler_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['refVaHex'])}</code></td>"
        f"<td>{html.escape(row['section'])}</td>"
        f"<td>{html.escape(', '.join(f'{item['table']}:{item['opcodeHex']}' for item in row.get('tableMatches') or []) or '-')}</td>"
        "</tr>"
        for row in handler["handlerPointerRefs"]
    ) or '<tr><td colspan="3">No handler refs.</td></tr>'
    global_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['targetHex'])}</code></td>"
        f"<td><code>{html.escape(row['refVaHex'])}</code></td>"
        f"<td>{html.escape(row['access'])}</td>"
        f"<td>{html.escape(row['instructionKind'])}</td>"
        "</tr>"
        for row in globals_["groupRefs"] + globals_["slotRefs"]
    )
    mode_rows = "\n".join(
        "<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.get('targetSelector') or '-')}</code></td>"
        f"<td>{html.escape(row_selector_text(row))}</td>"
        f"<td>{html.escape(str((row.get('prologuePattern') or {}).get('commonRouteSignature')))}</td>"
        f"<td>{html.escape(row['routeRole'])}</td>"
        "</tr>"
        for row in scan["routeContextMode1Rows"]
    ) or '<tr><td colspan="6">No route-context mode 1 rows.</td></tr>'
    cross_rows = "\n".join(
        "<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.get('targetSelector') or '-')}</code></td>"
        f"<td>{html.escape(row_selector_text(row))}</td>"
        f"<td>{html.escape(row['routeRole'])}</td>"
        "</tr>"
        for row in root_pattern["crossWriteRows"]
    ) or '<tr><td colspan="5">No cross writes.</td></tr>'
    continuation_summary_rows = []
    continuation_trace_rows = []
    for mode_key in ["mode1", "mode0"]:
        trace = continuation.get(f"{mode_key}Trace") or []
        first = trace[0] if trace else {}
        stop = next((row.get("stopReason") for row in trace if row.get("stopReason")), "-")
        continuation_summary_rows.append(
            "<tr>"
            f"<td><code>{html.escape(mode_key)}</code></td>"
            f"<td><code>{html.escape(continuation.get(f'{mode_key}ContinuationStreamHex') or '-')}</code></td>"
            f"<td><code>{html.escape(first.get('opcodeHex') or '-')}</code></td>"
            f"<td><code>{html.escape(first.get('handlerVaHex') or '-')}</code></td>"
            f"<td>{str(first.get('matchesSelectedRootConsumer') is True).lower()}</td>"
            f"<td>{html.escape(stop or '-')}</td>"
            "</tr>"
        )
        for row in trace:
            advances = "/".join(f"+{value}" for value in row.get("fixedAdvances", [])) or "-"
            continuation_trace_rows.append(
                "<tr>"
                f"<td><code>{html.escape(mode_key)}</code></td>"
                f"<td>{row.get('step')}</td>"
                f"<td><code>{html.escape(row.get('vaHex') or '-')}</code></td>"
                f"<td><code>{html.escape(row.get('valueHex') or '-')}</code></td>"
                f"<td><code>{html.escape(row.get('opcodeHex') or '-')}</code></td>"
                f"<td><code>{html.escape(row.get('handlerVaHex') or '-')}</code></td>"
                f"<td>{html.escape(advances)}</td>"
                f"<td>{html.escape(row.get('stopReason') or '-')}</td>"
                "</tr>"
            )
    continuation_preview_rows = "\n".join(
        "<tr>"
        f"<td>{row['index']}</td>"
        f"<td><code>{html.escape(row['vaHex'])}</code></td>"
        f"<td><code>{html.escape(row['valueHex'])}</code></td>"
        f"<td><code>{html.escape(row['lowByteOpcodeHex'])}</code></td>"
        f"<td>{row['u16Lo']}</td>"
        f"<td>{row['u16Hi']}</td>"
        f"<td>{html.escape(row['classification'])}</td>"
        "</tr>"
        for row in continuation.get("tablePreview") or []
    )

    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 Runtime Selector Byte Writes</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin: 16px 0 28px; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; position: sticky; top: 0; }",
        "    code { color: #f5d76e; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Runtime Selector Byte Writes</h1>",
        f"  <p>Route <code>{html.escape(summary['source'])}</code> -&gt; <code>{html.escape(summary['target'])}</code>; current selector <code>{summary['currentSelector']}</code> root <code>{summary['currentRootHex']}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p>{html.escape(summary['rootSelfWritePatternConclusion'])}</p>",
        "  <ul>",
        f"    <li>Handler <code>{handler['handlerVaHex']}</code>, general table <code>{handler['generalHandlerTableHex']}</code> opcode <code>{handler['opcodeHex']}</code>.</li>",
        f"    <li>Mode 1 self-write rows: {root_pattern['mode1SelfWriteCount']}; cross-write rows: {root_pattern['mode1CrossWriteCount']}.</li>",
        f"    <li>Mode 1 rows preceded by <code>{root_pattern['commonPreviousOpcode31Hex']}</code>: {root_pattern['mode1RowsPrecededByCommonOpcode31Count']}; next prefix <code>{', '.join(root_pattern['commonNextPrefixHex'])}</code>: {root_pattern['mode1RowsWithCommonNext6d05Count']}.</li>",
        f"    <li>Mode 1 current-selector writers: {scan['mode1CurrentSelectorWriterCount']}; outside current root: {scan['mode1CurrentSelectorWriterOutsideCurrentRootCount']}.</li>",
        f"    <li>Route-context self-write rows: {root_pattern['routeContextSelfWriteCount']}; common prologue signature rows: {root_pattern['routeContextCommonSignatureCount']}; cross writes to selector 2:0: {root_pattern['crossWriteToCurrentSelectorCount']}.</li>",
        f"    <li>source/predecessor current-selector writers: {summary['sourceOrPredecessorCurrentSelectorWriterCount']}.</li>",
        f"    <li>leaf-stream opcode 0x4f hits: {summary['leafStreamOpcode4fHitCount']}.</li>",
        f"    <li>mode1 continuation stream: <code>{html.escape(continuation.get('mode1ContinuationStreamHex') or '-')}</code>; starts with selected-root consumer: {continuation.get('mode1StartsWithSelectedRootConsumer')}.</li>",
        f"    <li>mode0 continuation stream: <code>{html.escape(continuation.get('mode0ContinuationStreamHex') or '-')}</code>; mode0 is mode1+4: {continuation.get('mode0IsMode1Plus4')}.</li>",
        f"    <li>root self-write pattern promotes route: {summary['rootSelfWritePatternPromotesRoute']}.</li>",
        f"    <li>selector byte write promotes route: {summary['selectorByteWritePromotesRoute']}.</li>",
        f"    <li>Promotion status: <code>{summary['promotionStatus']}</code>.</li>",
        "  </ul>",
        "  <h2>Continuation Stream Preview</h2>",
        f"  <p>{html.escape(continuation.get('promotionImpact') or '')}</p>",
        "  <table><thead><tr><th>mode</th><th>stream</th><th>first opcode</th><th>first handler</th><th>selected-root consumer</th><th>trace stop</th></tr></thead><tbody>",
        "\n".join(continuation_summary_rows),
        "  </tbody></table>",
        "  <h3>Continuation Trace</h3>",
        "  <table><thead><tr><th>mode</th><th>step</th><th>va</th><th>value</th><th>opcode</th><th>handler</th><th>advance</th><th>stop</th></tr></thead><tbody>",
        "\n".join(continuation_trace_rows),
        "  </tbody></table>",
        "  <h3>Continuation Table Preview</h3>",
        "  <table><thead><tr><th>index</th><th>va</th><th>value</th><th>low opcode</th><th>u16 lo</th><th>u16 hi</th><th>class</th></tr></thead><tbody>",
        continuation_preview_rows,
        "  </tbody></table>",
        "  <h2>Handler References</h2>",
        "  <table><thead><tr><th>ref</th><th>section</th><th>table match</th></tr></thead><tbody>",
        handler_rows,
        "  </tbody></table>",
        "  <h2>Selector Global Text Refs</h2>",
        "  <table><thead><tr><th>target</th><th>ref</th><th>access</th><th>instruction</th></tr></thead><tbody>",
        global_rows,
        "  </tbody></table>",
        "  <h2>Route Mode 1 Rows</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>target selector</th><th>root context</th><th>common prologue</th><th>role</th></tr></thead><tbody>",
        mode_rows,
        "  </tbody></table>",
        "  <h2>Cross Writes</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>target selector</th><th>root context</th><th>role</th></tr></thead><tbody>",
        cross_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_runtime_selector_byte_writes.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\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("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--leaf-streams", type=Path, default=OUT / "save_selector_leaf_streams.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        json.loads(args.selectors.read_text(encoding="utf-8")),
        json.loads(args.leaf_streams.read_text(encoding="utf-8")),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote runtime selector byte write summary "
        f"({summary['opcode4fDataScan']['mode1CurrentSelectorWriterCount']} current-selector writer) "
        f"-> {args.out_dir / 'save_selector_runtime_selector_byte_writes.json'}"
    )


if __name__ == "__main__":
    main()
