#!/usr/bin/env python3
"""Summarize the remaining battle display VM opcode semantics.

This report is intentionally narrow: it records which formerly vague display
VM opcodes are now grounded by handler/disassembly evidence and which parts
remain only presentation-label uncertain.
"""
from __future__ import annotations

import json
from collections import Counter
from pathlib import Path
from typing import Any


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


def load(name: str) -> dict[str, Any]:
    path = OUT / name
    if not path.exists():
        return {}
    return json.loads(path.read_text(encoding="utf-8"))


def count_decoded_opcodes(display: dict[str, Any]) -> Counter[str]:
    counts: Counter[str] = Counter()
    for entry in display.get("decodedRows", []):
        for row in entry.get("rows", []):
            opcode = row.get("opcode")
            if opcode:
                counts[opcode] += 1
    return counts


def example_for(display: dict[str, Any], opcode: str) -> dict[str, Any]:
    for entry in display.get("decodedRows", []):
        for row in entry.get("rows", []):
            if row.get("opcode") == opcode:
                return {
                    "owner": entry.get("ownerName"),
                    "skill": entry.get("skillName"),
                    "vaHex": row.get("vaHex"),
                    "bytes": row.get("bytes"),
                    "summary": row.get("summary"),
                    "semanticCategory": row.get("semanticCategory", row.get("category")),
                }
    return {}


def build() -> dict[str, Any]:
    display = load("battle_display_vm_static_decode.json")
    helper_body = load("battle_helper_body_review.json")
    helper_child = load("battle_helper_child_script_review.json")
    wait = load("battle_display_wait_opcode_review.json")
    sound = load("battle_sound_role_review.json")
    counts = count_decoded_opcodes(display)
    helper_summary = helper_body.get("summary", {})
    child_summary = helper_child.get("summary", {})
    wait_summary = wait.get("summary", {})
    sound_summary = sound.get("summary", {})
    rows = [
        {
            "opcode": "0x10",
            "status": "grounded",
            "semantic": "byte-width field operation",
            "handlerVaHex": "0x00402d2e",
            "evidence": "handler selects source by mode&0x30, destination by mode&0xc0, applies shared operation helper 0x00402f6f, then writes byte/dword by destination group",
            "count": counts.get("0x10", 0),
            "remaining": "visual label 없음; dataflow role is grounded",
        },
        {
            "opcode": "0x11",
            "status": "grounded",
            "semantic": "child/display field operation",
            "handlerVaHex": "0x0040308c",
            "evidence": "child/display object field writes use the same display field layout as helper child scripts; used for helper position/motion setup",
            "count": counts.get("0x11", 0),
            "remaining": "field names are functional labels from repeated consumers, not source-symbol names",
        },
        {
            "opcode": "0x12",
            "status": "grounded",
            "semantic": "dword display/actor field operation",
            "handlerVaHex": "0x0040308c",
            "evidence": "direct frame writes to +0x28 and repeated x/y/velocity/target field writes tie the field map to actor/helper motion",
            "count": counts.get("0x12", 0),
            "remaining": "field names are functional labels from repeated consumers, not source-symbol names",
        },
        {
            "opcode": "0x24",
            "status": "grounded",
            "semantic": "effect/cast WLK cue or effect handle helper",
            "handlerVaHex": "0x004045a0",
            "evidence": f"{sound_summary.get('effectCueCalls0x24', 0)} 0x24 effect cue calls; mode 0x01/arg1 0 resolves WLK/effect cue through 0x0042af73 and stores a handle",
            "count": counts.get("0x24", 0),
            "remaining": "whether a cue is best described as cast sound or helper/effect sound is presentation-level, not stream-boundary uncertainty",
        },
        {
            "opcode": "0x25",
            "status": "grounded",
            "semantic": "effect-control submode dispatch / global effect-slot flush-update",
            "handlerVaHex": "0x004047c1",
            "evidence": "submodes 0x00/0x01/0x80/0x81/0xf1 dispatch to known callees; 0xf1 calls 0x0042976b, which scans 16 global effect slots at 0x58d618..0x58d622, clears active flags, and invokes each slot object's vtable update/free callbacks after 비기·맹호유성각 helper 79 child-effect swarm",
            "count": counts.get("0x25", 0),
            "remaining": "presentation name can vary, but the control role is grounded",
        },
        {
            "opcode": "0xbd",
            "status": "grounded",
            "semantic": "helper-call dispatch",
            "handlerVaHex": helper_body.get("dispatcherEvidence", {}).get("handlerVaHex", "0x0040eab3"),
            "evidence": helper_body.get("dispatcherEvidence", {}).get("rule", "0xbd dispatch evidence missing"),
            "count": counts.get("0xbd", 0),
            "usedHelperIds": helper_summary.get("usedHelperIds", 0),
            "decodedChildScripts": child_summary.get("helpersWithDecodedChildScript", 0),
            "remaining": "helper visual behaviors are classified by child script/spawn tree; opcode dispatch itself is grounded",
        },
        {
            "opcode": "0xbf",
            "status": "grounded",
            "semantic": "display/actor busy barrier",
            "handlerVaHex": "0x0040ecdb",
            "evidence": "wait opcode review classifies 0xbf by surrounding actor flag/result signatures; it advances when active actor/display busy state clears",
            "count": counts.get("0xbf", 0),
            "semanticClassCounts": wait_summary.get("semanticClassCounts", {}),
            "remaining": "browser ms conversion remains approximate because this is a state barrier, not a fixed delay",
        },
        {
            "opcode": "0xc1",
            "status": "grounded",
            "semantic": "actor flag barrier",
            "handlerVaHex": "0x0040edb5",
            "evidence": f"mask distribution {wait_summary.get('c1MaskCounts', {})}; mode distribution {wait_summary.get('c1ModeCounts', {})}",
            "count": counts.get("0xc1", 0),
            "remaining": "browser ms conversion remains approximate because this is a flag barrier, not a fixed delay",
        },
    ]
    for row in rows:
        row["example"] = example_for(display, row["opcode"])
    return {
        "version": 1,
        "kind": "hwanse-battle-display-opcode-semantics-review",
        "source": "Hwanse2.exe",
        "status": "remaining-display-opcodes-audited",
        "runtimeUsed": False,
        "summary": {
            "auditedOpcodes": len(rows),
            "grounded": sum(1 for row in rows if row["status"] == "grounded"),
            "partiallyGrounded": sum(1 for row in rows if row["status"] == "partially-grounded"),
            "decodedRows": sum(counts.values()),
        },
        "interpretationNotes": [
            "0x10 is no longer treated as a vague placement helper; it is a byte field operation opcode.",
            "0xbd no longer means cleanup in the narrow sense.  It is a helper-call dispatch through 0x00411730/0x00454c10; some helpers perform cleanup-like behavior, but most spawn or control visuals.",
            "0xbf/0xc1 are state barriers.  They should not be converted directly to fixed milliseconds without runtime calibration.",
            "0x25 0xf1 is not a sprite/effect resource selector. It is a global effect-slot flush/update control used after child-effect swarm creation.",
        ],
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Display Opcode Semantics Review",
        "",
        f"- status: `{report['status']}`",
        f"- grounded: `{report['summary']['grounded']}` / `{report['summary']['auditedOpcodes']}`",
        "",
        "| opcode | status | semantic | handler | count | remaining |",
        "| --- | --- | --- | --- | ---: | --- |",
    ]
    for row in report["rows"]:
        lines.append(
            f"| `{row['opcode']}` | `{row['status']}` | {row['semantic']} | `{row['handlerVaHex']}` | {row['count']} | {row['remaining']} |"
        )
    lines += ["", "## Evidence", ""]
    for row in report["rows"]:
        lines += [
            f"### `{row['opcode']}`",
            "",
            f"- semantic: {row['semantic']}",
            f"- evidence: {row['evidence']}",
            f"- example: `{row.get('example', {}).get('bytes', '')}` at `{row.get('example', {}).get('vaHex', '')}` · {row.get('example', {}).get('summary', '')}",
            "",
        ]
    lines += ["## Notes", ""]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.append("")
    return "\n".join(lines)


def html_page(report: dict[str, Any]) -> str:
    def esc(value: Any) -> str:
        import html

        return html.escape(str(value if value is not None else ""))

    rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['opcode'])}</code></td>"
        f"<td>{esc(row['status'])}</td>"
        f"<td>{esc(row['semantic'])}</td>"
        f"<td><code>{esc(row['handlerVaHex'])}</code></td>"
        f"<td>{esc(row['count'])}</td>"
        f"<td>{esc(row['evidence'])}</td>"
        f"<td>{esc(row['remaining'])}</td>"
        "</tr>"
        for row in report["rows"]
    )
    examples = "".join(
        "<tr>"
        f"<td><code>{esc(row['opcode'])}</code></td>"
        f"<td>{esc(row.get('example', {}).get('owner', ''))}</td>"
        f"<td>{esc(row.get('example', {}).get('skill', ''))}</td>"
        f"<td><code>{esc(row.get('example', {}).get('vaHex', ''))}</code></td>"
        f"<td><code>{esc(row.get('example', {}).get('bytes', ''))}</code></td>"
        f"<td>{esc(row.get('example', {}).get('summary', ''))}</td>"
        "</tr>"
        for row in report["rows"]
    )
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="../favicon.ico">
  <title>Battle Display Opcode Semantics Review</title>
  <style>
    body {{ margin: 20px; background: #101114; color: #f1f3f5; font-family: system-ui, sans-serif; }}
    a {{ color: #9ecbff; }} code {{ color: #ffd37a; }}
    table {{ width: 100%; border-collapse: collapse; margin: 14px 0 24px; }}
    th, td {{ border: 1px solid #30343d; padding: 6px 8px; font-size: 12px; vertical-align: top; }}
    th {{ background: #1a1d24; color: #bac2cf; }}
  </style>
</head>
<body>
  <h1>Battle Display Opcode Semantics Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="battle_display_opcode_semantics_review.json">JSON</a> · <a href="battle_display_opcode_semantics_review.md">MD</a></p>
  <p>Status: <code>{esc(report['status'])}</code>. Grounded: <code>{esc(report['summary']['grounded'])}</code> / <code>{esc(report['summary']['auditedOpcodes'])}</code>.</p>
  <table><thead><tr><th>opcode</th><th>status</th><th>semantic</th><th>handler</th><th>count</th><th>evidence</th><th>remaining</th></tr></thead><tbody>{rows}</tbody></table>
  <h2>Examples</h2>
  <table><thead><tr><th>opcode</th><th>actor</th><th>skill</th><th>VA</th><th>bytes</th><th>summary</th></tr></thead><tbody>{examples}</tbody></table>
  <h2>Notes</h2>
  <ul>{notes}</ul>
</body>
</html>
"""


def main() -> None:
    report = build()
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "battle_display_opcode_semantics_review.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    (OUT / "battle_display_opcode_semantics_review.md").write_text(markdown(report), encoding="utf-8")
    (OUT / "battle_display_opcode_semantics_review.html").write_text(html_page(report), encoding="utf-8")
    print("wrote battle_display_opcode_semantics_review")


if __name__ == "__main__":
    main()
