#!/usr/bin/env python3
"""Summarize branch-state source candidates for the current selector root."""
from __future__ import annotations

import argparse
import html
import json
import struct
import sys
from pathlib import Path

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"


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


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


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


def word_bytes(value: int) -> dict:
    return {
        "byte0Hex": hex8(value & 0xFF),
        "byte1Hex": hex8((value >> 8) & 0xFF),
        "byte2Hex": hex8((value >> 16) & 0xFF),
        "byte3Hex": hex8((value >> 24) & 0xFF),
    }


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 pointer_ref_count(exe: bytes, sections: list[dict], target: int) -> int:
    pattern = struct.pack("<I", target)
    count = 0
    offset = 0
    while True:
        hit = exe.find(pattern, offset)
        if hit < 0:
            return count
        if any(section["raw"] <= hit < section["raw"] + section["raw_size"] for section in sections):
            count += 1
        offset = hit + 1


def operand_role(exe: bytes, sections: list[dict], va: int) -> dict | None:
    previous = dword_at(exe, sections, va - 4)
    if previous is None:
        return None
    previous_opcode = previous & 0xFF
    if previous_opcode == 0x11:
        state_selector = (previous >> 8) & 0xFF
        selection_offset = (previous >> 16) & 0xFF
        state_table = "primaryBranchState" if state_selector == 0 else "secondaryBranchState"
        operand_value = dword_at(exe, sections, va)
        operand_as_va = operand_value is not None and va_to_offset(sections, operand_value) is not None
        operand_kind = None
        if operand_value is not None:
            operand_kind = "va" if operand_as_va else "small-scalar-not-va" if operand_value < 0x10000 else "non-va-scalar"
        return {
            "kind": "branchTargetOperand",
            "ownerVaHex": hex32(va - 4),
            "ownerValueHex": hex32(previous),
            "ownerOpcodeHex": "0x11",
            "ownerStateSelectorByteHex": hex8(state_selector),
            "ownerStateTable": state_table,
            "ownerSelectionBufferOffset": selection_offset,
            "ownerSelectionBufferOffsetHex": hex8(selection_offset),
            "ownerCondition": f"{state_table}[selectionBuffer[0x{selection_offset:02x}]] == 1",
            "operandValueHex": hex32(operand_value) if operand_value is not None else None,
            "operandAsVa": operand_as_va,
            "operandValueKind": operand_kind,
            "linearOpcodeFalsePositive": True,
            "promotesPrimaryFill": False,
            "meaning": "opcode 0x11 reads this dword as its branch target operand, so this position is not a normal linear opcode entry.",
        }
    return None


def selector_root_range(root_hex: str, selectors: list[dict]) -> tuple[int, int]:
    root = int(root_hex, 16)
    roots = sorted({row["selectedPointer"] for row in selectors if isinstance(row.get("selectedPointer"), int)})
    next_roots = [value for value in roots if value > root]
    return root, next_roots[0] if next_roots else root + 0x4000


def decode_op10(value: int) -> dict | None:
    if (value & 0xFF) != 0x10:
        return None
    stream_plus_1 = (value >> 8) & 0xFF
    helper_arg = (value >> 16) & 0xFF
    table = "primaryBranchState" if stream_plus_1 == 0 else "secondaryBranchState"
    return {
        "valueHex": hex32(value),
        "streamPlus1Hex": hex8(stream_plus_1),
        "helperArgumentHex": hex8(helper_arg),
        "stateTable": table,
        "helperDispatchValid": helper_arg <= 0x0B,
        "meaning": (
            f"helper 0x00410c90 fills {table} with case 0x{helper_arg:02x}"
            if helper_arg <= 0x0B
            else f"helper 0x00410c90 receives out-of-range case 0x{helper_arg:02x}; no table fill"
        ),
    }


def collect_trace_members(current_writer_paths: list[dict]) -> tuple[set[str], set[str], set[str]]:
    activation = set()
    trace = set()
    writer_trace = set()
    for row in current_writer_paths:
        for item in (row.get("activationContext") or {}).get("contextRows") or []:
            if item.get("vaHex"):
                activation.add(item["vaHex"])
        for item in row.get("trace") or []:
            if item.get("vaHex"):
                trace.add(item["vaHex"])
                if item.get("opcodeHex") == "0x12":
                    writer_trace.add(item["vaHex"])
    return activation, trace, writer_trace


def nearby_words(exe: bytes, sections: list[dict], center_va: int, before: int = 5, after: int = 5) -> list[dict]:
    rows = []
    for relative_index in range(-before, after + 1):
        va = center_va + relative_index * 4
        value = dword_at(exe, sections, va)
        if value is None:
            continue
        rows.append({
            "relativeIndex": relative_index,
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            **word_bytes(value),
        })
    return rows


def branch_operand_context(
    exe: bytes,
    sections: list[dict],
    row: dict,
) -> dict | None:
    role = row.get("operandRole") or {}
    if role.get("kind") != "branchTargetOperand":
        return None
    owner_va = parse_hex(role.get("ownerVaHex"))
    operand_va = parse_hex(row.get("vaHex"))
    if owner_va is None or operand_va is None:
        return None
    owner_value = dword_at(exe, sections, owner_va)
    operand_value = dword_at(exe, sections, operand_va)
    if owner_value is None or operand_value is None:
        return None
    operand_as_va = va_to_offset(sections, operand_value) is not None
    operand_kind = "va" if operand_as_va else "small-scalar-not-va" if operand_value < 0x10000 else "non-va-scalar"
    return {
        "ownerVaHex": hex32(owner_va),
        "ownerValueHex": hex32(owner_value),
        "ownerOpcodeHex": hex8(owner_value & 0xFF),
        "ownerStateSelectorByteHex": role.get("ownerStateSelectorByteHex"),
        "ownerStateTable": role.get("ownerStateTable"),
        "ownerSelectionBufferOffset": role.get("ownerSelectionBufferOffset"),
        "ownerSelectionBufferOffsetHex": role.get("ownerSelectionBufferOffsetHex"),
        "ownerCondition": role.get("ownerCondition"),
        "ownerPointerRefCount": pointer_ref_count(exe, sections, owner_va),
        "operandVaHex": hex32(operand_va),
        "operandValueHex": hex32(operand_value),
        "operandPointerRefCount": row.get("pointerRefCount", 0),
        "operandAsVa": operand_as_va,
        "operandValueKind": operand_kind,
        "operandWouldDecodeAsStateTable": row.get("stateTable"),
        "operandWouldDecodeAsHelperArgumentHex": row.get("helperArgumentHex"),
        "operandWouldDecodeAsHelperDispatchValid": row.get("helperDispatchValid"),
        "nearbyWords": nearby_words(exe, sections, owner_va),
        "linearOpcodeFalsePositive": True,
        "promotesPrimaryFill": False,
        "explanation": (
            f"{row.get('vaHex')} is the branch-target operand consumed by opcode 0x11 at "
            f"{role.get('ownerVaHex')}, not a linear opcode 0x10 helper-fill row."
        ),
    }


def build_summary(
    exe: bytes,
    selection_writers: dict,
    current_writer_paths: list[dict],
    selectors: list[dict],
) -> dict:
    sections = read_sections(exe)
    root_hexes = selection_writers.get("currentFrontierRootHexes") or []
    root_hex = root_hexes[0] if root_hexes else "0x00000000"
    root_start, root_end = selector_root_range(root_hex, selectors)
    first_frontier_reader = min(
        (
            parse_hex(row.get("vaHex"))
            for row in selection_writers.get("currentFrontierRows") or []
            if parse_hex(row.get("vaHex")) is not None
        ),
        default=None,
    )
    activation_vas, trace_vas, writer_trace_vas = collect_trace_members(current_writer_paths)
    candidates = []
    for va in range(root_start, root_end, 4):
        value = dword_at(exe, sections, va)
        if value is None:
            continue
        decoded = decode_op10(value)
        if not decoded:
            continue
        va_hex = hex32(va)
        refs = pointer_ref_count(exe, sections, va)
        role = operand_role(exe, sections, va)
        executed = va_hex in activation_vas or va_hex in trace_vas
        candidates.append({
            "vaHex": va_hex,
            **decoded,
            "beforeFirstFrontierReader": first_frontier_reader is not None and va < first_frontier_reader,
            "inCurrentWriterActivationContext": va_hex in activation_vas,
            "inCurrentWriterTrace": va_hex in trace_vas,
            "inCurrentWriterOpcode12Trace": va_hex in writer_trace_vas,
            "pointerRefCount": refs,
            "operandRole": role,
            "hasExecutionEvidence": executed,
            "confidence": (
                "executed activation context"
                if va_hex in activation_vas
                else "executed writer trace"
                if va_hex in trace_vas
                else "branch operand dword"
                if role
                else "root-range dword candidate"
            ),
        })
    activation_candidates = [row for row in candidates if row["inCurrentWriterActivationContext"]]
    valid_before_frontier = [
        row for row in candidates
        if row["beforeFirstFrontierReader"] and row["helperDispatchValid"]
    ]
    valid_with_execution = [
        row for row in valid_before_frontier
        if row["hasExecutionEvidence"]
    ]
    valid_without_operand_role = [
        row for row in valid_before_frontier
        if not row.get("operandRole")
    ]
    promoting_valid_before_frontier = [
        row for row in valid_before_frontier
        if row["hasExecutionEvidence"] and not row.get("operandRole")
    ]
    branch_operand_candidates = [
        row for row in candidates
        if (row.get("operandRole") or {}).get("kind") == "branchTargetOperand"
    ]
    branch_operand_contexts = [
        context
        for row in branch_operand_candidates
        for context in [branch_operand_context(exe, sections, row)]
        if context is not None
    ]
    branch_operand_small_scalar_count = sum(
        1 for context in branch_operand_contexts
        if context.get("operandValueKind") == "small-scalar-not-va"
    )
    valid_activation = [row for row in activation_candidates if row["helperDispatchValid"]]
    out_of_range_activation = [row for row in activation_candidates if not row["helperDispatchValid"]]
    return {
        "rootHex": root_hex,
        "rootRangeHex": f"{hex32(root_start)}..{hex32(root_end)}",
        "firstFrontierReaderHex": hex32(first_frontier_reader) if first_frontier_reader is not None else None,
        "helper": {
            "handlerVaHex": "0x00410c90",
            "validCaseRangeHex": "0x00..0x0b",
            "calledByOpcode10HandlerHex": "0x0040b49e",
        },
        "candidateCount": len(candidates),
        "validBeforeFirstFrontierReaderCount": len(valid_before_frontier),
        "validBeforeFirstFrontierReaderWithExecutionEvidenceCount": len(valid_with_execution),
        "validBeforeFirstFrontierReaderNonOperandCount": len(valid_without_operand_role),
        "validBeforeFirstFrontierReaderPromotingFillCount": len(promoting_valid_before_frontier),
        "branchOperandCandidateCount": len(branch_operand_candidates),
        "branchOperandSmallScalarCount": branch_operand_small_scalar_count,
        "branchOperandVaHex": branch_operand_candidates[0].get("vaHex") if branch_operand_candidates else None,
        "branchOperandOwnerVaHex": (
            (branch_operand_candidates[0].get("operandRole") or {}).get("ownerVaHex")
            if branch_operand_candidates
            else None
        ),
        "branchOperandOwnerCondition": (
            (branch_operand_candidates[0].get("operandRole") or {}).get("ownerCondition")
            if branch_operand_candidates
            else None
        ),
        "activationCandidateCount": len(activation_candidates),
        "validActivationCandidateCount": len(valid_activation),
        "outOfRangeActivationCandidateCount": len(out_of_range_activation),
        "conclusion": (
            "The current writer activation trace contains no valid opcode 0x10 helper fill. "
            "Its opcode 0x10 rows pass 0x20, outside helper 0x00410c90's 0x00..0x0b dispatch range. "
            "The only root-range valid-looking candidate before the frontier is 0x00542244, the small-scalar branch-target "
            "operand of opcode 0x11 at 0x00542240, not an executable linear opcode 0x10 helper-fill row. "
            "primaryBranchState is inherited from earlier runtime state at this point."
        ),
        "candidates": candidates,
        "activationCandidates": activation_candidates,
        "validBeforeFirstFrontierReader": valid_before_frontier,
        "validBeforeFirstFrontierReaderPromotingFills": promoting_valid_before_frontier,
        "branchOperandContexts": branch_operand_contexts,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Current State Sources",
        "",
        "Opcode `0x10` branch-state source candidates in the current selector root.",
        "",
        f"- root: `{summary['rootHex']}` (`{summary['rootRangeHex']}`)",
        f"- first frontier reader: `{summary['firstFrontierReaderHex']}`",
        f"- helper: `{summary['helper']['handlerVaHex']}`, valid dispatch cases `{summary['helper']['validCaseRangeHex']}`",
        f"- activation-context `0x10` candidates: {summary['activationCandidateCount']}",
        f"- valid activation-context fills: {summary['validActivationCandidateCount']}",
        f"- root-range valid fills before first frontier reader: {summary['validBeforeFirstFrontierReaderCount']}",
        f"- root-range valid fills with execution evidence: {summary['validBeforeFirstFrontierReaderWithExecutionEvidenceCount']}",
        f"- root-range valid fills that are not branch operands: {summary['validBeforeFirstFrontierReaderNonOperandCount']}",
        f"- root-range valid promoting fills: {summary['validBeforeFirstFrontierReaderPromotingFillCount']}",
        f"- branch operand false positives: {summary['branchOperandCandidateCount']} (small scalar: {summary['branchOperandSmallScalarCount']})",
        "",
        summary["conclusion"],
        "",
        "## Activation Context",
        "",
        "| va | value | table | helper arg | valid? | confidence | meaning |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["activationCandidates"]:
        lines.append(
            f"| {row['vaHex']} | `{row['valueHex']}` | {row['stateTable']} | "
            f"{row['helperArgumentHex']} | {'yes' if row['helperDispatchValid'] else 'no'} | "
            f"{row['confidence']} | {row['meaning']} |"
        )
    if not summary["activationCandidates"]:
        lines.append("| - | - | - | - | - | - | - |")
    lines.extend([
        "",
        "## Valid Root-Range Candidates Before Frontier",
        "",
        "| va | value | table | helper arg | activation trace? | pointer refs | operand role | confidence |",
        "| --- | --- | --- | --- | --- | ---: | --- | --- |",
    ])
    for row in summary["validBeforeFirstFrontierReader"]:
        role = row.get("operandRole") or {}
        lines.append(
            f"| {row['vaHex']} | `{row['valueHex']}` | {row['stateTable']} | "
            f"{row['helperArgumentHex']} | {'yes' if row['inCurrentWriterActivationContext'] else 'no'} | "
            f"{row.get('pointerRefCount', 0)} | {role.get('kind') or '-'} {role.get('ownerVaHex') or ''} | "
            f"{row['confidence']} |"
        )
    if not summary["validBeforeFirstFrontierReader"]:
        lines.append("| - | - | - | - | - | - | - | - |")
    lines.extend([
        "",
        "## Branch Operand False Positive",
        "",
        "| owner | owner value | condition | operand | operand value | operand kind | would decode as | promoting fill? |",
        "| --- | --- | --- | --- | --- | --- | --- | --- |",
    ])
    for context in summary.get("branchOperandContexts") or []:
        lines.append(
            f"| {context['ownerVaHex']} | `{context['ownerValueHex']}` | `{context.get('ownerCondition')}` | "
            f"{context['operandVaHex']} | `{context['operandValueHex']}` | {context['operandValueKind']} | "
            f"{context.get('operandWouldDecodeAsStateTable')} case {context.get('operandWouldDecodeAsHelperArgumentHex')} | "
            f"{'yes' if context.get('promotesPrimaryFill') else 'no'} |"
        )
    if not summary.get("branchOperandContexts"):
        lines.append("| - | - | - | - | - | - | - | - |")
    lines.extend([
        "",
        "### Nearby Words",
        "",
        "| va | value | b0 | b1 | b2 | b3 |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    nearby = (summary.get("branchOperandContexts") or [{}])[0].get("nearbyWords") or []
    for word in nearby:
        lines.append(
            f"| {word['vaHex']} | `{word['valueHex']}` | {word['byte0Hex']} | "
            f"{word['byte1Hex']} | {word['byte2Hex']} | {word['byte3Hex']} |"
        )
    if not nearby:
        lines.append("| - | - | - | - | - | - |")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    def row_html(row: dict, include_meaning: bool) -> str:
        role = row.get("operandRole") or {}
        cells = [
            f"<td><code>{html.escape(row['vaHex'])}</code></td>",
            f"<td><code>{html.escape(row['valueHex'])}</code></td>",
            f"<td>{html.escape(row['stateTable'])}</td>",
            f"<td><code>{html.escape(row['helperArgumentHex'])}</code></td>",
            f"<td>{'yes' if row['helperDispatchValid'] else 'no'}</td>",
            f"<td>{html.escape(row['confidence'])}</td>",
        ]
        if include_meaning:
            cells.append(f"<td>{html.escape(row['meaning'])}</td>")
        else:
            cells.extend([
                f"<td>{row.get('pointerRefCount', 0)}</td>",
                f"<td>{html.escape((role.get('kind') or '-') + (' ' + role.get('ownerVaHex') if role.get('ownerVaHex') else ''))}</td>",
            ])
        return "<tr>" + "".join(cells) + "</tr>"

    activation = "\n".join(row_html(row, True) for row in summary["activationCandidates"])
    valid = "\n".join(row_html(row, False) for row in summary["validBeforeFirstFrontierReader"])
    branch_context_rows = []
    for context in summary.get("branchOperandContexts") or []:
        branch_context_rows.append(
            "<tr>"
            f"<td><code>{html.escape(context['ownerVaHex'])}</code></td>"
            f"<td><code>{html.escape(context['ownerValueHex'])}</code></td>"
            f"<td><code>{html.escape(context.get('ownerCondition') or '-')}</code></td>"
            f"<td><code>{html.escape(context['operandVaHex'])}</code></td>"
            f"<td><code>{html.escape(context['operandValueHex'])}</code></td>"
            f"<td>{html.escape(context.get('operandValueKind') or '-')}</td>"
            f"<td>{html.escape(str(context.get('promotesPrimaryFill')))}</td>"
            f"<td>{html.escape(context.get('explanation') or '')}</td>"
            "</tr>"
        )
    nearby_rows = []
    nearby = (summary.get("branchOperandContexts") or [{}])[0].get("nearbyWords") or []
    for word in nearby:
        nearby_rows.append(
            "<tr>"
            f"<td><code>{html.escape(word['vaHex'])}</code></td>"
            f"<td><code>{html.escape(word['valueHex'])}</code></td>"
            f"<td><code>{html.escape(word['byte0Hex'])}</code></td>"
            f"<td><code>{html.escape(word['byte1Hex'])}</code></td>"
            f"<td><code>{html.escape(word['byte2Hex'])}</code></td>"
            f"<td><code>{html.escape(word['byte3Hex'])}</code></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 Current State Sources</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 Current State Sources</h1>",
        f"  <p>Root <code>{html.escape(summary['rootHex'])}</code> range <code>{html.escape(summary['rootRangeHex'])}</code>. First frontier reader <code>{html.escape(summary['firstFrontierReaderHex'] or '-')}</code>.</p>",
        f"  <p>Helper <code>{html.escape(summary['helper']['handlerVaHex'])}</code> accepts cases <code>{html.escape(summary['helper']['validCaseRangeHex'])}</code>.</p>",
        f"  <p>valid activation-context fills: {summary['validActivationCandidateCount']}</p>",
        f"  <p>root-range valid fills with execution evidence: {summary['validBeforeFirstFrontierReaderWithExecutionEvidenceCount']}</p>",
        f"  <p>root-range valid promoting fills: {summary['validBeforeFirstFrontierReaderPromotingFillCount']}</p>",
        f"  <p>branch operand false positives: {summary['branchOperandCandidateCount']} (small scalar: {summary['branchOperandSmallScalarCount']})</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Activation Context</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>table</th><th>helper arg</th><th>valid?</th><th>confidence</th><th>meaning</th></tr></thead>",
        f"  <tbody>{activation or '<tr><td colspan=\"7\">No activation candidates.</td></tr>'}</tbody></table>",
        "  <h2>Valid Root-Range Candidates Before Frontier</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>table</th><th>helper arg</th><th>valid?</th><th>confidence</th><th>pointer refs</th><th>operand role</th></tr></thead>",
        f"  <tbody>{valid or '<tr><td colspan=\"8\">No valid candidates.</td></tr>'}</tbody></table>",
        "  <h2>Branch Operand False Positive</h2>",
        "  <table><thead><tr><th>owner</th><th>owner value</th><th>condition</th><th>operand</th><th>operand value</th><th>operand kind</th><th>promoting fill?</th><th>explanation</th></tr></thead>",
        f"  <tbody>{''.join(branch_context_rows) or '<tr><td colspan=\"8\">No branch operand false positives.</td></tr>'}</tbody></table>",
        "  <h3>Nearby Words</h3>",
        "  <table><thead><tr><th>va</th><th>value</th><th>b0</th><th>b1</th><th>b2</th><th>b3</th></tr></thead>",
        f"  <tbody>{''.join(nearby_rows) or '<tr><td colspan=\"6\">No nearby words.</td></tr>'}</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_current_state_sources.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_current_state_sources.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--selection-writers", type=Path, default=OUT / "save_selector_selection_writers.json")
    parser.add_argument("--current-writer-paths", type=Path, default=OUT / "save_selector_current_writer_paths.json")
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        json.loads(args.selection_writers.read_text(encoding="utf-8")),
        json.loads(args.current_writer_paths.read_text(encoding="utf-8")),
        json.loads(args.selectors.read_text(encoding="utf-8")),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote current state source summary "
        f"({summary['validActivationCandidateCount']} valid activation fills) "
        f"-> {args.out_dir / 'save_selector_current_state_sources.html'}"
    )


if __name__ == "__main__":
    main()
