#!/usr/bin/env python3
"""Scan object field references relevant to opcode 0x24 state updates."""
from __future__ import annotations

import argparse
import html
import json
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"
OFFSETS = {
    0x58: "object/context state",
    0x59: "object action/resource selector",
    0x61: "opcode24 object slot/state field",
    0x67: "object state switch input",
}
ROUTE_RELEVANT_VAS = {
    0x0040C5A0: "opcode24 mode0 writes current object index to object+0x61",
    0x0040C5F3: "opcode24 mode0 writes first available slot+3 to object+0x61",
    0x0040C680: "opcode24 mode1 writes 0x0059e348+3 to object+0x61",
    0x0040C690: "opcode24 mode2 writes 0x0059e347 to object+0x61",
    0x0040C58D: "opcode24 mode0 reads object+0x67 state switch input",
    0x0040C56D: "opcode24 mode0 reads object+0x58 state",
}


def instruction_kind(opcode: int, modrm: int) -> str:
    if opcode == 0x8A:
        return "read"
    if opcode == 0x88:
        return "write"
    if opcode == 0xC6:
        return "write-immediate"
    return "unknown"


def scan_refs(exe: bytes) -> dict:
    sections = read_sections(exe)
    text = next(section for section in sections if section["name"] == ".text")
    data = exe[text["raw"]: text["raw"] + text["raw_size"]]
    rows = []
    for pos in range(len(data) - 4):
        opcode = data[pos]
        if opcode not in {0x8A, 0x88, 0xC6}:
            continue
        displacement = data[pos + 2]
        if displacement not in OFFSETS:
            continue
        va = text["va"] + pos
        raw_len = 4 if opcode != 0xC6 else 5
        rows.append({
            "vaHex": f"0x{va:08x}",
            "offsetHex": f"0x{displacement:02x}",
            "offset": displacement,
            "field": OFFSETS[displacement],
            "access": instruction_kind(opcode, data[pos + 1]),
            "rawHex": data[pos:pos + raw_len].hex(" "),
            "routeNote": ROUTE_RELEVANT_VAS.get(va),
        })
    by_offset = {}
    for offset in OFFSETS:
        offset_rows = [row for row in rows if row["offset"] == offset]
        by_offset[f"0x{offset:02x}"] = {
            "field": OFFSETS[offset],
            "total": len(offset_rows),
            "reads": sum(1 for row in offset_rows if row["access"] == "read"),
            "writes": sum(1 for row in offset_rows if row["access"].startswith("write")),
        }
    refs_by_offset = {
        f"0x{offset:02x}": [row for row in rows if row["offset"] == offset]
        for offset in OFFSETS
    }
    object61 = refs_by_offset["0x61"]
    conclusion = (
        "The current opcode 0x24 mode 1 write at 0x0040c680 is one of several object+0x61 writes. "
        "There are also object+0x61 readers in nearby object/runtime handlers, but this static field-reference scan "
        "does not identify a direct save-selector leaf jump. It narrows the next task to consumers around "
        "0x0040cc6d..0x0040ef09 and helper routines around 0x004334a3..0x004355f0."
    )
    return {
        "scope": "simple .text scan for byte object-field references with 8-bit displacement",
        "offsetSummary": by_offset,
        "fieldRefsByOffset": refs_by_offset,
        "object61Refs": object61,
        "routeRelevantRefs": [row for row in rows if row.get("routeNote")],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Object Field References",
        "",
        f"Scope: {summary.get('scope')}.",
        "",
        f"- conclusion: {summary.get('conclusion')}",
        "",
        "## Offset Summary",
        "",
        "| offset | field | refs | reads | writes |",
        "| --- | --- | ---: | ---: | ---: |",
    ]
    for offset, row in (summary.get("offsetSummary") or {}).items():
        lines.append(f"| `{offset}` | {row.get('field')} | {row.get('total')} | {row.get('reads')} | {row.get('writes')} |")
    lines.extend([
        "",
        "## object+0x61 References",
        "",
        "| va | access | raw | route note |",
        "| --- | --- | --- | --- |",
    ])
    for row in summary.get("object61Refs") or []:
        lines.append(
            f"| `{row.get('vaHex')}` | {row.get('access')} | `{row.get('rawHex')}` | {row.get('routeNote') or '-'} |"
        )
    context58 = ((summary.get("fieldRefsByOffset") or {}).get("0x58") or [])
    lines.extend([
        "",
        "## object/context+0x58 References",
        "",
        "| va | access | raw | route note |",
        "| --- | --- | --- | --- |",
    ])
    for row in context58:
        lines.append(
            f"| `{row.get('vaHex')}` | {row.get('access')} | `{row.get('rawHex')}` | {row.get('routeNote') or '-'} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    summary_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(offset))}</code></td>"
        f"<td>{html.escape(str(row.get('field')))}</td>"
        f"<td>{row.get('total')}</td>"
        f"<td>{row.get('reads')}</td>"
        f"<td>{row.get('writes')}</td>"
        "</tr>"
        for offset, row in (summary.get("offsetSummary") or {}).items()
    )
    ref_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('vaHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('access')))}</td>"
        f"<td><code>{html.escape(str(row.get('rawHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('routeNote') or '-'))}</td>"
        "</tr>"
        for row in summary.get("object61Refs") or []
    )
    context58_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('vaHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('access')))}</td>"
        f"<td><code>{html.escape(str(row.get('rawHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('routeNote') or '-'))}</td>"
        "</tr>"
        for row in ((summary.get("fieldRefsByOffset") or {}).get("0x58") or [])
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Object Field References</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee}table{border-collapse:collapse}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Object Field References</h1>",
        f"<p>Scope: {html.escape(str(summary.get('scope')))}.</p>",
        f"<p>{html.escape(str(summary.get('conclusion')))}</p>",
        "<h2>Offset Summary</h2>",
        "<table><thead><tr><th>offset</th><th>field</th><th>refs</th><th>reads</th><th>writes</th></tr></thead><tbody>",
        summary_rows,
        "</tbody></table>",
        "<h2>object+0x61 References</h2>",
        "<table><thead><tr><th>va</th><th>access</th><th>raw</th><th>route note</th></tr></thead><tbody>",
        ref_rows,
        "</tbody></table>",
        "<h2>object/context+0x58 References</h2>",
        "<table><thead><tr><th>va</th><th>access</th><th>raw</th><th>route note</th></tr></thead><tbody>",
        context58_rows,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_object_field_refs.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 = scan_refs(args.exe.read_bytes())
    write_outputs(summary, args.out_dir)
    print(f"wrote object field refs -> {args.out_dir / 'save_selector_object_field_refs.json'}")


if __name__ == "__main__":
    main()
