#!/usr/bin/env python3
"""Summarize object+0x61 consumer patterns around opcode 0x24."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path


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

GROUPS = [
    {
        "name": "linked object draw/copy",
        "range": "0x0040cc6d..0x0040cc8f",
        "refs": ["0x0040cc6d", "0x0040cc8f"],
        "evidence": [
            "mov cl,[current+0x61]; object = runtimeObjectTable[cl]",
            "test linkedObject+0x62 bit 0x20",
            "call 0x00402360 with current object+0x88 and linked object resource pointer",
        ],
        "routeImpact": "Consumes object+0x61 as another runtime object index; no stream pointer or scene record write is visible.",
    },
    {
        "name": "linked object replacement",
        "range": "0x0040d006..0x0040d02d",
        "refs": ["0x0040d006", "0x0040d02d"],
        "evidence": [
            "mov cl,[current+0x61]; object = runtimeObjectTable[cl]",
            "test linkedObject+0x62 bit 0x80",
            "call 0x00433aca and write returned byte back to current+0x61",
        ],
        "routeImpact": "Updates the linked object index when the target object is flagged; this is object state maintenance, not leaf selection.",
    },
    {
        "name": "collision/action mask test",
        "range": "0x0040d3af..0x0040d4d8",
        "refs": ["0x0040d3af"],
        "evidence": [
            "current object can be replaced by runtimeObjectTable[current+0x61]",
            "reads selected object+0x04 mode",
            "tests object+0x5c masks and sets global 0x0055a1b8",
        ],
        "routeImpact": "Tests object action/collision masks and advances the current stream by +4 on the non-trigger path.",
    },
    {
        "name": "object spawn/flag management",
        "range": "0x0040d5da..0x0040dded",
        "refs": ["0x0040d5da", "0x0040dcc7"],
        "evidence": [
            "uses object+0x61 to select a linked runtime object",
            "checks and writes object+0x5b, object+0x5c, object+0x60, object+0x62",
            "some branches replace context+0x40 with dword [stream+4]",
        ],
        "routeImpact": "Can branch the current script stream, but through the active handler's stream operand, not through the save-selector leaf table.",
    },
    {
        "name": "status sync helper",
        "range": "0x0040de28..0x0040ef09",
        "refs": ["0x0040de28", "0x0040e240", "0x0040eaf4", "0x0040ee6c", "0x0040ef09"],
        "evidence": [
            "selects linked runtime objects through runtimeObjectTable[object+0x61]",
            "copies object flags/position/status fields into other object-side tables",
            "does not directly reference map1_01a, map2_02d, or save-selector leaf addresses",
        ],
        "routeImpact": "Runtime object synchronization; useful for VM semantics, weak evidence for map transition promotion.",
    },
    {
        "name": "generic object helpers",
        "range": "0x004334a3..0x004355f0",
        "refs": ["0x004334a3", "0x00433ad8", "0x00433afe", "0x00433b0b", "0x00433db3", "0x00433efe", "0x004355e5", "0x004355f0"],
        "evidence": [
            "helper routines read/write object+0x61 while scanning runtime object slots",
            "called by several object VM handlers including opcode 0x24 mode 0 support code",
            "acts on object relationships rather than field-map scene records",
        ],
        "routeImpact": "Confirms object+0x61 is a general linked-object slot, not a dedicated save-selector route selector.",
    },
]


def load_object_refs(out_dir: Path) -> dict:
    path = out_dir / "save_selector_object_field_refs.json"
    return json.loads(path.read_text(encoding="utf-8"))


def build_summary(out_dir: Path) -> dict:
    object_refs = load_object_refs(out_dir)
    refs_by_va = {row.get("vaHex"): row for row in object_refs.get("object61Refs") or []}
    rows = []
    for group in GROUPS:
        refs = [refs_by_va.get(ref, {"vaHex": ref, "missing": True}) for ref in group["refs"]]
        rows.append({**group, "object61Refs": refs})
    conclusion = (
        "The opcode 0x24 mode 1 write to object+0x61 feeds runtime object-link consumers. "
        "Those consumers dereference runtimeObjectTable[object+0x61] or maintain object flags; they do not provide a direct "
        "map1_01a->map2_02d save-selector leaf jump. Continue route work from branch-state/stream control-flow evidence, "
        "not from object+0x61 alone."
    )
    return {
        "source": "save_selector_object_field_refs.json",
        "groups": rows,
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector object+0x61 Consumers",
        "",
        f"- source: `{summary.get('source')}`",
        f"- conclusion: {summary.get('conclusion')}",
        "",
        "| group | range | refs | evidence | route impact |",
        "| --- | --- | --- | --- | --- |",
    ]
    for group in summary.get("groups") or []:
        refs = ", ".join(f"`{row.get('vaHex')}`" for row in group.get("object61Refs") or [])
        evidence = "<br>".join(html.escape(item) for item in group.get("evidence") or [])
        lines.append(
            f"| {group.get('name')} | `{group.get('range')}` | {refs} | {evidence} | {group.get('routeImpact')} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(str(group.get('name')))}</td>"
        f"<td><code>{html.escape(str(group.get('range')))}</code></td>"
        f"<td>{', '.join('<code>' + html.escape(str(row.get('vaHex'))) + '</code>' for row in group.get('object61Refs') or [])}</td>"
        f"<td>{'<br>'.join(html.escape(str(item)) for item in group.get('evidence') or [])}</td>"
        f"<td>{html.escape(str(group.get('routeImpact')))}</td>"
        "</tr>"
        for group in summary.get("groups") or []
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector object+0x61 Consumers</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee}table{border-collapse:collapse;max-width:1400px}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector object+0x61 Consumers</h1>",
        f"<p>Source: <code>{html.escape(str(summary.get('source')))}</code></p>",
        f"<p>{html.escape(str(summary.get('conclusion')))}</p>",
        "<table><thead><tr><th>group</th><th>range</th><th>refs</th><th>evidence</th><th>route impact</th></tr></thead><tbody>",
        rows,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_object61_consumers.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.out_dir)
    write_outputs(summary, args.out_dir)
    print(f"wrote object+0x61 consumer summary -> {args.out_dir / 'save_selector_object61_consumers.json'}")


if __name__ == "__main__":
    main()
