#!/usr/bin/env python3
"""Review battle display VM wait opcodes from static EXE-derived data only.

The browser runner still needs approximate wall-clock values when it cannot
execute the original display state machine.  This report keeps those preview
numbers out of the evidence layer: opcode 0xbf and 0xc1 are classified here as
state barriers, not as fixed delays.
"""
from __future__ import annotations

import html
import json
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
DISPLAY_JSON = OUT / "battle_display_vm_static_decode.json"
WAIT_SEMANTICS_JSON = OUT / "battle_actor_display_wait_semantics_review.json"


def esc(value: Any) -> str:
    return html.escape(str(value if value is not None else ""))


def hex32(value: int | None) -> str:
    if value is None:
        return ""
    return f"0x{value & 0xffffffff:08x}"


def parse_bytes(row: dict[str, Any]) -> list[int]:
    out: list[int] = []
    for part in str(row.get("bytes") or "").split():
        try:
            out.append(int(part, 16))
        except ValueError:
            return []
    return out


def parse_c1_operand(row: dict[str, Any]) -> tuple[int | None, int | None]:
    mode = row.get("mode")
    mask = row.get("mask")
    if isinstance(mode, int) and isinstance(mask, int):
        return mode, mask
    raw = parse_bytes(row)
    if len(raw) >= 8 and raw[0] == 0xC1:
        return raw[1], raw[4] | (raw[5] << 8) | (raw[6] << 16) | (raw[7] << 24)
    return None, None


def compact_instr(row: dict[str, Any]) -> dict[str, Any]:
    out = {
        "vaHex": row.get("vaHex"),
        "opcode": row.get("opcode"),
        "category": row.get("category"),
        "summary": row.get("summary"),
    }
    for key in ("mode", "maskHex", "normalWlkNo", "altWlkNo", "wlkNo", "frame", "gate", "helperId"):
        if row.get(key) not in (None, "", []):
            out[key] = row.get(key)
    return out


def category_signature(rows: list[dict[str, Any]]) -> list[str]:
    return [f"{row.get('opcode')}:{row.get('category')}" for row in rows]


def has_result_flag_sequence(previous: list[dict[str, Any]]) -> bool:
    if len(previous) < 3:
        return False
    tail = previous[-3:]
    if [row.get("opcode") for row in tail] != ["0xc2", "0xad", "0xad"]:
        return False
    return (
        tail[1].get("mode") == 2
        and tail[1].get("maskHex") == "0x0000000c"
        and tail[2].get("mode") == 1
        and tail[2].get("maskHex") == "0x00000008"
    )


def classify_wait(row: dict[str, Any], previous: list[dict[str, Any]], following: list[dict[str, Any]]) -> tuple[str, str]:
    opcode = row.get("opcode")
    if opcode == "0xbf":
        if has_result_flag_sequence(previous):
            return (
                "result-reaction-barrier",
                "결과음 0xc2와 hit-window flag set/clear 직후에 걸리는 barrier. 데미지/피격 반응 표시가 끝날 때까지 다음 VM 명령 진행을 막는 용도다.",
            )
        if any(item.get("category") == "actor-flags" for item in previous[-3:]):
            return (
                "actor-flag-busy-barrier",
                "actor flag 조작 직후의 busy barrier. 일반 sleep이 아니라 display actor 상태가 풀릴 때까지 대기한다.",
            )
        return (
            "general-busy-barrier",
            "busy/action wait gate. 주변 문맥이 결과 표시로 한정되지는 않지만 고정 ms 지연으로 해석할 근거는 없다.",
        )
    if opcode == "0xc1":
        mode, mask = parse_c1_operand(row)
        if mask == 0x200:
            return (
                "pierce-motion-actor-flag-barrier",
                "관통/돌진 계열에서 frame selector와 motion opcode 뒤에 나타나는 actor flag barrier. 이동 완료 latch를 기다리는 패턴이다.",
            )
        if mode == 1:
            return (
                "inverse-actor-flag-barrier",
                "mode=1 actor flag barrier. 린샹 회복/지원 계열에서 확인되며, 대상/지원 display 상태가 특정 mask와 반대로 정리될 때까지 기다린다.",
            )
        if mask == 0x02 and any(item.get("opcode") == "0xc2" for item in following[:4]):
            return (
                "pre-result-actor-flag-barrier",
                "타격음/결과 flag 전에 놓이는 actor flag barrier. helper나 시전자 frame 동작이 완료된 뒤 결과 판정 표시로 넘어간다.",
            )
        if mask == 0x02:
            return (
                "actor-flag-barrier",
                "mask 0x00000002 actor flag barrier. 주변 기술별 helper/frame 흐름에 따라 다음 action segment를 기다린다.",
            )
        return (
            "actor-flag-barrier-unknown-mask",
            f"mode={mode}, mask={hex32(mask)} actor flag barrier. operand는 확인됐지만 의미 명칭은 아직 일반 actor flag 대기로 둔다.",
        )
    return "unknown-wait", "unknown wait opcode"


def build() -> dict[str, Any]:
    display = json.loads(DISPLAY_JSON.read_text(encoding="utf-8"))
    wait_semantics = json.loads(WAIT_SEMANTICS_JSON.read_text(encoding="utf-8")) if WAIT_SEMANTICS_JSON.exists() else {}
    rows: list[dict[str, Any]] = []
    opcode_counts: Counter[str] = Counter()
    semantic_counts: Counter[str] = Counter()
    c1_mode_counts: Counter[str] = Counter()
    c1_mask_counts: Counter[str] = Counter()
    bf_prev_signature_counts: Counter[str] = Counter()
    c1_next_signature_counts: Counter[str] = Counter()
    owner_counts: defaultdict[str, Counter[str]] = defaultdict(Counter)

    for decoded in display.get("decodedRows") or []:
        instrs = decoded.get("rows") or []
        for index, instr in enumerate(instrs):
            if instr.get("opcode") not in {"0xbf", "0xc1"}:
                continue
            previous = instrs[max(0, index - 5): index]
            following = instrs[index + 1: index + 6]
            semantic_class, interpretation = classify_wait(instr, previous, following)
            opcode = str(instr.get("opcode"))
            opcode_counts[opcode] += 1
            semantic_counts[semantic_class] += 1
            owner_counts[str(decoded.get("ownerKey"))][opcode] += 1
            if opcode == "0xbf":
                bf_prev_signature_counts[" > ".join(category_signature(previous[-3:]))] += 1
            if opcode == "0xc1":
                mode, mask = parse_c1_operand(instr)
                c1_mode_counts[str(mode)] += 1
                c1_mask_counts[hex32(mask)] += 1
                c1_next_signature_counts[" > ".join(category_signature(following[:3]))] += 1
            rows.append(
                {
                    "ownerKey": decoded.get("ownerKey"),
                    "ownerName": decoded.get("ownerName"),
                    "skillName": decoded.get("skillName"),
                    "familyName": decoded.get("familyName"),
                    "skillIdHex": decoded.get("skillIdHex"),
                    "entryStartVaHex": decoded.get("entryStartVaHex"),
                    "waitVaHex": instr.get("vaHex"),
                    "opcode": opcode,
                    "bytes": instr.get("bytes"),
                    "mode": parse_c1_operand(instr)[0] if opcode == "0xc1" else None,
                    "maskHex": hex32(parse_c1_operand(instr)[1]) if opcode == "0xc1" else "",
                    "semanticClass": semantic_class,
                    "interpretation": interpretation,
                    "previous": [compact_instr(item) for item in previous[-5:]],
                    "following": [compact_instr(item) for item in following[:5]],
                }
            )

    return {
        "version": 1,
        "kind": "hwanse-battle-display-wait-opcode-review",
        "source": [
            "out/battle_display_vm_static_decode.json",
            "out/battle_actor_display_wait_semantics_review.json",
            "Hwanse2.exe static disassembly only",
        ],
        "status": "static-display-wait-opcodes-grounded",
        "summary": {
            "waitRows": len(rows),
            "opcodeCounts": dict(sorted(opcode_counts.items())),
            "semanticClassCounts": dict(sorted(semantic_counts.items())),
            "c1ModeCounts": dict(sorted(c1_mode_counts.items())),
            "c1MaskCounts": dict(sorted(c1_mask_counts.items())),
            "ownerOpcodeCounts": {owner: dict(sorted(counts.items())) for owner, counts in sorted(owner_counts.items())},
            "topBfPreviousSignatures": dict(bf_prev_signature_counts.most_common(8)),
            "topC1FollowingSignatures": dict(c1_next_signature_counts.most_common(8)),
        },
        "interpretationNotes": [
            "0xbf and 0xc1 are VM wait barriers, not wall-clock millisecond constants.",
            "0xbf is strongly tied to the result/reaction window: 205/205 occurrences follow actor flag rows, and the dominant pattern is 0xc2 result WLK -> 0xad set 0x0c -> 0xad clear 0x08 -> 0xbf.",
            "0xc1 carries explicit actor flag operands. Static decode finds mode 0 or 1 and masks 0x00000002 or 0x00000200.",
            "The exact browser preview ms cannot be derived from these opcodes alone. The original engine advances display actors each tick and the wait loop proceeds once relevant flags/skip bits settle.",
            "This report intentionally excludes Wine/runtime capture evidence.",
        ],
        "waitLoopEvidenceSummary": {
            "status": wait_semantics.get("status"),
            "summary": wait_semantics.get("summary"),
            "coreMeaning": [
                item.get("meaning")
                for item in wait_semantics.get("wait", [])
                if item.get("subject") in {"0x40e0c2 wait loop", "wait completion", "actor +0x62 bit 0x80"}
            ],
        },
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Display Wait Opcode Review",
        "",
        f"- status: `{report['status']}`",
        f"- wait rows: `{report['summary']['waitRows']}`",
        f"- opcode counts: `{report['summary']['opcodeCounts']}`",
        f"- c1 masks: `{report['summary']['c1MaskCounts']}`",
        "",
        "## 해석",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend(
        [
            "",
            "## Rows",
            "",
            "| actor | skill | id | wait | opcode | class | bytes | operand |",
            "| --- | --- | ---: | --- | --- | --- | --- | --- |",
        ]
    )
    for row in report["rows"]:
        operand = f"mode={row.get('mode')} mask={row.get('maskHex')}" if row.get("opcode") == "0xc1" else "-"
        lines.append(
            f"| {row.get('ownerName')} | {row.get('skillName')} | `{row.get('skillIdHex')}` | `{row.get('waitVaHex')}` | "
            f"`{row.get('opcode')}` | `{row.get('semanticClass')}` | `{row.get('bytes')}` | {operand} |"
        )
    return "\n".join(lines) + "\n"


def html_page(report: dict[str, Any]) -> str:
    rows_html = []
    for row in report["rows"]:
        operand = f"mode={row.get('mode')} mask={row.get('maskHex')}" if row.get("opcode") == "0xc1" else "-"
        prev = "<br>".join(f"{esc(item.get('opcode'))} {esc(item.get('summary'))}" for item in row.get("previous") or [])
        nxt = "<br>".join(f"{esc(item.get('opcode'))} {esc(item.get('summary'))}" for item in row.get("following") or [])
        rows_html.append(
            "<tr>"
            f"<td>{esc(row.get('ownerName'))}</td>"
            f"<td>{esc(row.get('skillName'))}<br><code>{esc(row.get('skillIdHex'))}</code></td>"
            f"<td><code>{esc(row.get('waitVaHex'))}</code><br><code>{esc(row.get('bytes'))}</code></td>"
            f"<td><code>{esc(row.get('opcode'))}</code><br>{esc(operand)}</td>"
            f"<td><strong>{esc(row.get('semanticClass'))}</strong><br>{esc(row.get('interpretation'))}</td>"
            f"<td>{prev}</td>"
            f"<td>{nxt}</td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Battle Display Wait Opcode Review</title>
<style>
  body {{ margin:24px; font-family:system-ui,-apple-system,Segoe UI,sans-serif; background:#f7f8fa; color:#17202a; }}
  h1 {{ margin:0 0 8px; font-size:24px; }}
  h2 {{ margin:22px 0 10px; font-size:18px; }}
  a {{ color:#185abc; text-decoration:none; }}
  a:hover {{ text-decoration:underline; }}
  .nav {{ display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 18px; }}
  .nav a, .pill {{ border:1px solid #d8dee6; background:#fff; border-radius:5px; padding:5px 9px; font-size:13px; }}
  .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:10px; }}
  .card {{ border:1px solid #d8dee6; border-radius:7px; background:#fff; padding:10px; }}
  .card strong {{ display:block; font-size:20px; }}
  ul {{ background:#fff; border:1px solid #d8dee6; border-radius:7px; padding:12px 18px; }}
  table {{ width:100%; border-collapse:collapse; background:#fff; border:1px solid #d8dee6; }}
  th,td {{ border-bottom:1px solid #d8dee6; padding:8px 10px; vertical-align:top; font-size:13px; }}
  th {{ background:#eef2f7; text-align:left; }}
  code {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; }}
  .table-wrap {{ overflow:auto; }}
</style>
</head>
<body>
<h1>Battle Display Wait Opcode Review</h1>
<div class="nav">
  <a href="../web/index.html">index</a>
  <a href="../web/battle_simulator.html">battle runner</a>
  <a href="battle_display_vm_static_decode.html">display VM decode</a>
  <a href="battle_actor_display_wait_semantics_review.html">actor wait semantics</a>
  <a href="battle_display_wait_opcode_review.json">JSON</a>
  <a href="battle_display_wait_opcode_review.md">MD</a>
</div>
<div class="cards">
  <div class="card"><span>Wait rows</span><strong>{esc(report['summary']['waitRows'])}</strong></div>
  <div class="card"><span>Opcode counts</span><strong>{esc(report['summary']['opcodeCounts'])}</strong></div>
  <div class="card"><span>C1 masks</span><strong>{esc(report['summary']['c1MaskCounts'])}</strong></div>
</div>
<h2>해석</h2>
<ul>{"".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])}</ul>
<h2>Wait Rows</h2>
<div class="table-wrap">
<table>
<thead><tr><th>actor</th><th>skill</th><th>wait</th><th>operand</th><th>class</th><th>previous</th><th>following</th></tr></thead>
<tbody>{"".join(rows_html)}</tbody>
</table>
</div>
</body>
</html>
"""


def main() -> None:
    report = build()
    (OUT / "battle_display_wait_opcode_review.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    (OUT / "battle_display_wait_opcode_review.md").write_text(markdown(report), encoding="utf-8")
    (OUT / "battle_display_wait_opcode_review.html").write_text(html_page(report), encoding="utf-8")
    print("wrote out/battle_display_wait_opcode_review.{json,md,html}")


if __name__ == "__main__":
    main()
