#!/usr/bin/env python3
"""Summarize globals used by save-selector opcode 0x24 mode dispatch."""
from __future__ import annotations

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

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"

GLOBALS = {
    "0x0059e348": "opcode24 mode1 source byte",
    "0x0059e347": "opcode24 mode2 source byte",
    "0x0059e33e": "current runtime object index",
    "0x0059e34d": "runtime object/event enabled flag",
}

NEIGHBORHOOD_START = 0x0059E330
NEIGHBORHOOD_END = 0x0059E350


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def classify_access(section_data: bytes, pos: int) -> dict:
    before = section_data[max(0, pos - 8):pos]
    after = section_data[pos + 4:pos + 12]
    if before.endswith(b"\xa0"):
        return {"kind": "read", "instruction": "mov al, ds:[addr]"}
    if before.endswith(b"\xa2"):
        return {"kind": "write", "instruction": "mov ds:[addr], al"}
    if before.endswith(b"\x8a\x0d"):
        return {"kind": "read", "instruction": "mov cl, byte ptr ds:[addr]"}
    if before.endswith(b"\x8a\x15"):
        return {"kind": "read", "instruction": "mov dl, byte ptr ds:[addr]"}
    if before.endswith(b"\x88\x0d"):
        return {"kind": "write", "instruction": "mov byte ptr ds:[addr], cl"}
    if before.endswith(b"\x88\x15"):
        return {"kind": "write", "instruction": "mov byte ptr ds:[addr], dl"}
    if after and before:
        return {"kind": "unknown", "instruction": "unclassified direct address reference"}
    return {"kind": "unknown", "instruction": "unclassified"}


def refs_for_value(exe: bytes, sections: list[dict], value: int) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    for section in sections:
        if section.get("name") != ".text":
            continue
        start = section["raw"]
        end = start + section["raw_size"]
        data = exe[start:end]
        pos = data.find(needle)
        while pos >= 0:
            access = classify_access(data, pos)
            refs.append({
                "refVaHex": f"0x{section['va'] + pos:08x}",
                "instructionVaHex": f"0x{section['va'] + max(0, pos - instruction_prefix_len(data, pos)):08x}",
                "accessKind": access["kind"],
                "instruction": access["instruction"],
            })
            pos = data.find(needle, pos + 1)
    return refs


def instruction_prefix_len(data: bytes, pos: int) -> int:
    for size, suffixes in [
        (2, [b"\x8a\x0d", b"\x8a\x15", b"\x88\x0d", b"\x88\x15"]),
        (1, [b"\xa0", b"\xa2"]),
    ]:
        if pos >= size and any(data[pos - size:pos] == suffix for suffix in suffixes):
            return size
    return 0


def build_summary(exe: bytes, handler_summary: dict) -> dict:
    sections = read_sections(exe)
    rows = []
    for value_hex, meaning in GLOBALS.items():
        refs = refs_for_value(exe, sections, int(value_hex, 16))
        rows.append({
            "globalVaHex": value_hex,
            "meaning": meaning,
            "directTextRefCount": len(refs),
            "directReadCount": sum(1 for ref in refs if ref.get("accessKind") == "read"),
            "directWriteCount": sum(1 for ref in refs if ref.get("accessKind") == "write"),
            "refs": refs,
        })
    neighborhood_rows = []
    for address in range(NEIGHBORHOOD_START, NEIGHBORHOOD_END):
        address_hex = f"0x{address:08x}"
        refs = refs_for_value(exe, sections, address)
        if not refs:
            continue
        neighborhood_rows.append({
            "globalVaHex": address_hex,
            "meaning": GLOBALS.get(address_hex, ""),
            "directTextRefCount": len(refs),
            "directReadCount": sum(1 for ref in refs if ref.get("accessKind") == "read"),
            "directWriteCount": sum(1 for ref in refs if ref.get("accessKind") == "write"),
            "refs": refs,
        })
    mode1 = next(row for row in rows if row["globalVaHex"] == "0x0059e348")
    mode2 = next(row for row in rows if row["globalVaHex"] == "0x0059e347")
    current_object = next(row for row in rows if row["globalVaHex"] == "0x0059e33e")
    mode1_neighborhood = next(row for row in neighborhood_rows if row["globalVaHex"] == "0x0059e348")
    direct_write_rows = [
        row for row in neighborhood_rows
        if row.get("directWriteCount", 0) > 0
    ]
    direct_write_addresses = sorted(
        int(row["globalVaHex"], 16)
        for row in direct_write_rows
        if row.get("globalVaHex")
    )
    mode1_address = int("0x0059e348", 16)
    lower_writes = [address for address in direct_write_addresses if address < mode1_address]
    higher_writes = [address for address in direct_write_addresses if address > mode1_address]
    nearest_lower_write = max(lower_writes) if lower_writes else None
    nearest_higher_write = min(higher_writes) if higher_writes else None
    mode1_direct_write_hole = (
        mode1_neighborhood["directWriteCount"] == 0
        and nearest_lower_write is not None
        and nearest_higher_write is not None
    )
    mode2_writer_context = {
        "writerInstructionVaHexes": [
            ref.get("instructionVaHex")
            for ref in mode2.get("refs") or []
            if ref.get("accessKind") == "write"
        ],
        "sourceReadInstructionVaHex": "0x0040fb1c",
        "sourceGlobalVaHex": "0x0059e33e",
        "sourceMeaning": GLOBALS["0x0059e33e"],
        "targetGlobalVaHex": "0x0059e347",
        "targetMeaning": GLOBALS["0x0059e347"],
        "copiesCurrentRuntimeObjectIndex": any(
            ref.get("instructionVaHex") == "0x0040fb1c"
            for ref in current_object.get("refs") or []
        ) and any(
            ref.get("instructionVaHex") == "0x0040fb21"
            for ref in mode2.get("refs") or []
            if ref.get("accessKind") == "write"
        ),
        "instructionSummary": "mov al, ds:[0x0059e33e]; mov ds:[0x0059e347], al",
        "promotionImpact": "adjacent mode2 object-index cache, not a mode1 0x0059e348 producer",
    }
    conclusion = (
        "Opcode 0x24 mode 1 reads 0x0059e348, but no direct .text write to 0x0059e348 was found. "
        "The adjacent mode 2 byte 0x0059e347 does have a direct writer at 0x0040fb1c that copies "
        "the current runtime object index 0x0059e33e. A wider 0x0059e330..0x0059e34f neighborhood scan shows "
        "that nearby object/status bytes are heavily referenced while 0x0059e348 still has only that single read "
        "and no direct writer. Direct writes are present on both sides of the mode1 source, so the scan is seeing "
        "adjacent runtime bytes but leaves 0x0059e348 as a direct-write hole. This makes 0x0059e348 an unresolved "
        "runtime/global source rather than a proven selector leaf chooser."
    )
    return {
        "scope": "direct .text references to opcode 0x24 runtime globals",
        "currentOpcode24Mode": (handler_summary.get("currentBoundary") or {}).get("streamPlus1Hex"),
        "rows": rows,
        "neighborhoodRangeHex": f"0x{NEIGHBORHOOD_START:08x}..0x{NEIGHBORHOOD_END:08x}",
        "neighborhoodRows": neighborhood_rows,
        "mode1NeighborhoodDirectReadCount": mode1_neighborhood["directReadCount"],
        "mode1NeighborhoodDirectWriteCount": mode1_neighborhood["directWriteCount"],
        "mode1DirectWriteCount": mode1["directWriteCount"],
        "mode2DirectWriteCount": mode2["directWriteCount"],
        "neighborhoodDirectWriteAddressHexes": [
            f"0x{address:08x}" for address in direct_write_addresses
        ],
        "mode1NearestLowerDirectWriteHex": (
            f"0x{nearest_lower_write:08x}" if nearest_lower_write is not None else None
        ),
        "mode1NearestHigherDirectWriteHex": (
            f"0x{nearest_higher_write:08x}" if nearest_higher_write is not None else None
        ),
        "mode1DirectWriteHoleBetweenNeighborWrites": mode1_direct_write_hole,
        "mode1UnwrittenRuntimeGlobalSource": mode1["directWriteCount"] == 0 and mode1_neighborhood["directWriteCount"] == 0,
        "mode2WriterContext": mode2_writer_context,
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x24 Globals",
        "",
        f"Scope: {summary.get('scope')}.",
        "",
        f"- current opcode 0x24 mode: `{summary.get('currentOpcode24Mode')}`",
        f"- mode1 direct writes: {summary.get('mode1DirectWriteCount')}",
        f"- mode2 direct writes: {summary.get('mode2DirectWriteCount')}",
        f"- mode1 unwritten runtime/global source: {summary.get('mode1UnwrittenRuntimeGlobalSource')}",
        f"- mode2 writer context: {((summary.get('mode2WriterContext') or {}).get('instructionSummary'))}",
        f"- mode2 promotion impact: {((summary.get('mode2WriterContext') or {}).get('promotionImpact'))}",
        f"- neighborhood range: `{summary.get('neighborhoodRangeHex')}`",
        f"- mode1 neighborhood reads/writes: {summary.get('mode1NeighborhoodDirectReadCount')} / {summary.get('mode1NeighborhoodDirectWriteCount')}",
        f"- neighborhood direct-write addresses: {', '.join(summary.get('neighborhoodDirectWriteAddressHexes') or []) or '-'}",
        f"- mode1 nearest lower/higher direct writes: `{summary.get('mode1NearestLowerDirectWriteHex')}` / `{summary.get('mode1NearestHigherDirectWriteHex')}`",
        f"- mode1 direct-write hole between neighbor writes: {summary.get('mode1DirectWriteHoleBetweenNeighborWrites')}",
        f"- conclusion: {summary.get('conclusion')}",
        "",
        "| global | meaning | refs | reads | writes | direct refs |",
        "| --- | --- | ---: | ---: | ---: | --- |",
    ]
    for row in summary.get("rows") or []:
        refs = "<br>".join(
            f"{ref.get('instructionVaHex')} {ref.get('accessKind')} {ref.get('instruction')}"
            for ref in row.get("refs") or []
        ) or "-"
        lines.append(
            f"| `{row.get('globalVaHex')}` | {row.get('meaning')} | {row.get('directTextRefCount')} | "
            f"{row.get('directReadCount')} | {row.get('directWriteCount')} | {refs} |"
        )
    lines.extend([
        "",
        "## Neighborhood Direct Refs",
        "",
        "| global | meaning | refs | reads | writes | sample refs |",
        "| --- | --- | ---: | ---: | ---: | --- |",
    ])
    for row in summary.get("neighborhoodRows") or []:
        refs = "<br>".join(
            f"{ref.get('instructionVaHex')} {ref.get('accessKind')} {ref.get('instruction')}"
            for ref in (row.get("refs") or [])[:8]
        ) or "-"
        lines.append(
            f"| `{row.get('globalVaHex')}` | {row.get('meaning') or '-'} | {row.get('directTextRefCount')} | "
            f"{row.get('directReadCount')} | {row.get('directWriteCount')} | {refs} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    body = []
    for row in summary.get("rows") or []:
        refs = "<br>".join(
            html.escape(f"{ref.get('instructionVaHex')} {ref.get('accessKind')} {ref.get('instruction')}")
            for ref in row.get("refs") or []
        ) or "-"
        body.append(
            "<tr>"
            f"<td><code>{html.escape(str(row.get('globalVaHex')))}</code></td>"
            f"<td>{html.escape(str(row.get('meaning')))}</td>"
            f"<td>{row.get('directTextRefCount')}</td>"
            f"<td>{row.get('directReadCount')}</td>"
            f"<td>{row.get('directWriteCount')}</td>"
            f"<td>{refs}</td>"
            "</tr>"
        )
    neighborhood_body = []
    for row in summary.get("neighborhoodRows") or []:
        refs = "<br>".join(
            html.escape(f"{ref.get('instructionVaHex')} {ref.get('accessKind')} {ref.get('instruction')}")
            for ref in (row.get("refs") or [])[:8]
        ) or "-"
        neighborhood_body.append(
            "<tr>"
            f"<td><code>{html.escape(str(row.get('globalVaHex')))}</code></td>"
            f"<td>{html.escape(str(row.get('meaning') or '-'))}</td>"
            f"<td>{row.get('directTextRefCount')}</td>"
            f"<td>{row.get('directReadCount')}</td>"
            f"<td>{row.get('directWriteCount')}</td>"
            f"<td>{refs}</td>"
            "</tr>"
        )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Opcode 0x24 Globals</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 Opcode 0x24 Globals</h1>",
        f"<p>Scope: {html.escape(str(summary.get('scope')))}.</p>",
        "<ul>",
        f"<li>current opcode 0x24 mode: <code>{html.escape(str(summary.get('currentOpcode24Mode')))}</code></li>",
        f"<li>mode1 direct writes: {summary.get('mode1DirectWriteCount')}</li>",
        f"<li>mode2 direct writes: {summary.get('mode2DirectWriteCount')}</li>",
        f"<li>mode1 unwritten runtime/global source: {summary.get('mode1UnwrittenRuntimeGlobalSource')}</li>",
        f"<li>mode2 writer context: {html.escape(str((summary.get('mode2WriterContext') or {}).get('instructionSummary')))}</li>",
        f"<li>mode2 promotion impact: {html.escape(str((summary.get('mode2WriterContext') or {}).get('promotionImpact')))}</li>",
        f"<li>neighborhood range: <code>{html.escape(str(summary.get('neighborhoodRangeHex')))}</code></li>",
        f"<li>mode1 neighborhood reads/writes: {summary.get('mode1NeighborhoodDirectReadCount')} / {summary.get('mode1NeighborhoodDirectWriteCount')}</li>",
        f"<li>neighborhood direct-write addresses: {html.escape(', '.join(summary.get('neighborhoodDirectWriteAddressHexes') or []) or '-')}</li>",
        f"<li>mode1 nearest lower/higher direct writes: <code>{html.escape(str(summary.get('mode1NearestLowerDirectWriteHex')))}</code> / <code>{html.escape(str(summary.get('mode1NearestHigherDirectWriteHex')))}</code></li>",
        f"<li>mode1 direct-write hole between neighbor writes: {summary.get('mode1DirectWriteHoleBetweenNeighborWrites')}</li>",
        f"<li>{html.escape(str(summary.get('conclusion')))}</li>",
        "</ul>",
        "<table><thead><tr><th>global</th><th>meaning</th><th>refs</th><th>reads</th><th>writes</th><th>direct refs</th></tr></thead><tbody>",
        "\n".join(body),
        "</tbody></table>",
        "<h2>Neighborhood Direct Refs</h2>",
        "<table><thead><tr><th>global</th><th>meaning</th><th>refs</th><th>reads</th><th>writes</th><th>sample refs</th></tr></thead><tbody>",
        "\n".join(neighborhood_body),
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode24_globals.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), 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)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "save_selector_opcode24_handler.json", {}),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote opcode 0x24 globals summary -> {args.out_dir / 'save_selector_opcode24_globals.json'}")


if __name__ == "__main__":
    main()
