#!/usr/bin/env python3
"""Summarize the object+0x61 consumer group that can branch the active stream."""
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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
GROUP_START = 0x0040D5DA
GROUP_END = 0x0040DDED
FRONTIER_VALUES = {
    0x00542AE8: "frontier leaf pointer",
    0x00542B0C: "frontier reader",
    0x0053F46F: "reader false branch target",
}


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


def text_refs(exe: bytes, values: dict[int, str]) -> dict[str, list[str]]:
    sections = read_sections(exe)
    text = next(section for section in sections if section["name"] == ".text")
    data = exe[text["raw"]:text["raw"] + text["raw_size"]]
    refs: dict[str, list[str]] = {}
    for value in values:
        needle = struct.pack("<I", value)
        hits = []
        pos = data.find(needle)
        while pos >= 0:
            hits.append(hex32(text["va"] + pos))
            pos = data.find(needle, pos + 1)
        refs[hex32(value)] = hits
    return refs


def build_summary(exe: bytes) -> dict:
    refs = text_refs(exe, FRONTIER_VALUES)
    direct_ref_count = sum(len(items) for items in refs.values())
    stream_effects = [
        {
            "vaHex": "0x0040d66e",
            "effect": "if local flag remains set, context+0x40 = dword [stream+4]; otherwise stream += 8",
            "routeImpact": "generic branch through active handler operand",
        },
        {
            "vaHex": "0x0040d6f9",
            "effect": "no free object slot path sets global 0x0059e334=1 and context+0x40 = dword [stream+4]",
            "routeImpact": "object allocation failure branch, not a save-selector leaf table reference",
        },
        {
            "vaHex": "0x0040dd86",
            "effect": "linked object action path sets global 0x0059e334=2 and context+0x40 = dword [stream+4]",
            "routeImpact": "object action/status branch, still through the current stream operand",
        },
        {
            "vaHex": "0x0040ddf2",
            "effect": "non-trigger path advances context+0x40 by 8",
            "routeImpact": "fixed local advance",
        },
    ]
    conclusion = (
        "The object+0x61 consumer group can replace the active VM stream with dword [stream+4], but no direct .text "
        "reference links this group to the current save-selector leaf 0x00542ae8, reader 0x00542b0c, or false branch "
        "target 0x0053f46f. It is a generic object/action branch mechanism. It should not be used to promote "
        "map1_01a->map2_02d without a concrete active stream operand that points at the frontier leaf."
    )
    return {
        "scope": "object+0x61 consumer group 0x0040d5da..0x0040dded that may write context+0x40 from dword [stream+4]",
        "groupRangeHex": f"{hex32(GROUP_START)}..{hex32(GROUP_END)}",
        "frontierRefs": [
            {
                "valueHex": hex32(value),
                "meaning": meaning,
                "textRefs": refs[hex32(value)],
            }
            for value, meaning in FRONTIER_VALUES.items()
        ],
        "directFrontierRefCount": direct_ref_count,
        "streamEffects": stream_effects,
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector object+0x61 Stream Branch",
        "",
        f"Scope: {summary['scope']}.",
        "",
        f"- group range: `{summary['groupRangeHex']}`",
        f"- direct frontier refs: {summary['directFrontierRefCount']}",
        f"- promotion status: {summary['promotionStatus']}",
        f"- conclusion: {summary['conclusion']}",
        "",
        "## Frontier Refs",
        "",
        "| value | meaning | text refs |",
        "| --- | --- | --- |",
    ]
    for row in summary["frontierRefs"]:
        refs = ", ".join(f"`{ref}`" for ref in row["textRefs"]) or "-"
        lines.append(f"| `{row['valueHex']}` | {row['meaning']} | {refs} |")
    lines.extend(["", "## Stream Effects", "", "| va | effect | route impact |", "| --- | --- | --- |"])
    for row in summary["streamEffects"]:
        lines.append(f"| `{row['vaHex']}` | {row['effect']} | {row['routeImpact']} |")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    refs = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['valueHex'])}</code></td>"
        f"<td>{html.escape(row['meaning'])}</td>"
        f"<td>{html.escape(', '.join(row['textRefs']) or '-')}</td>"
        "</tr>"
        for row in summary["frontierRefs"]
    )
    effects = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['vaHex'])}</code></td>"
        f"<td>{html.escape(row['effect'])}</td>"
        f"<td>{html.escape(row['routeImpact'])}</td>"
        "</tr>"
        for row in summary["streamEffects"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector object+0x61 Stream Branch</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector object+0x61 Stream Branch</h1>",
        f"<p>Scope: {html.escape(summary['scope'])}.</p>",
        f"<p>direct frontier refs: {summary['directFrontierRefCount']}; promotion status: {html.escape(summary['promotionStatus'])}</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Frontier Refs</h2>",
        "<table><thead><tr><th>value</th><th>meaning</th><th>text refs</th></tr></thead><tbody>",
        refs,
        "</tbody></table>",
        "<h2>Stream Effects</h2>",
        "<table><thead><tr><th>va</th><th>effect</th><th>route impact</th></tr></thead><tbody>",
        effects,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_object61_stream_branch.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        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)
    args = parser.parse_args()
    summary = build_summary(args.exe.read_bytes())
    write_outputs(summary, args.out_dir)
    print(f"wrote object+0x61 stream branch -> {args.out_dir / 'save_selector_object61_stream_branch.json'}")


if __name__ == "__main__":
    main()
