#!/usr/bin/env python3
"""Document battle actor display wait/update semantics."""
from __future__ import annotations

import html
import json
import re
import subprocess
from pathlib import Path
from typing import Any


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


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


def disassemble(start_va: int, stop_va: int) -> str:
    result = subprocess.run(
        [
            "objdump",
            "-Mintel",
            "-D",
            "-b",
            "pei-i386",
            f"--start-address=0x{start_va:08x}",
            f"--stop-address=0x{stop_va:08x}",
            str(EXE),
        ],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    return "\n".join(
        line for line in result.stdout.splitlines()
        if re.match(r"\s*[0-9a-f]{6,8}:", line)
    )


def compact_lines(disasm: str, needles: list[str], context: int = 3) -> list[str]:
    lines = disasm.splitlines()
    selected: list[tuple[int, str]] = []
    for index, line in enumerate(lines):
        if any(needle in line for needle in needles):
            for i in range(max(0, index - context), min(len(lines), index + context + 1)):
                selected.append((i, lines[i]))
    seen: set[int] = set()
    out: list[str] = []
    for index, line in selected:
        if index not in seen:
            seen.add(index)
            out.append(line)
    return out


WAIT_ROWS = [
    {
        "subject": "0x40e0c2 wait loop",
        "meaning": "0x4576e8 active slot count만큼 순회한다. actor +0x62 bit 0x80이 켜진 actor는 0x42177f 호출 없이 스킵한다.",
        "confidence": "확정",
    },
    {
        "subject": "0x42177f return",
        "meaning": "함수 끝에서 AL=1을 반환한다. 반환값은 개별 애니메이션 완료 판정보다 '이번 tick에 non-skipped actor를 갱신했음'에 가깝다.",
        "confidence": "확정",
    },
    {
        "subject": "wait completion",
        "meaning": "모든 actor가 +0x62 bit 0x80로 스킵되거나 active slot이 없으면 OR accumulator가 0으로 남아 다음 VM 명령으로 진행한다.",
        "confidence": "확정",
    },
    {
        "subject": "actor +0x62 bit 0x80",
        "meaning": "display wait skip/completed bit. phase 4 예약 경로에서 명시적으로 켜지며, script byte OR opcode도 이 비트를 actor set에 전파할 수 있다.",
        "confidence": "강한 추정",
    },
]


SLOT_FIELD_ROWS = [
    {"slotField": "+0x06", "actorField": "+0x06", "source": "incremented until 0x63", "meaning": "slot update age/tick. 0x42177f는 slot +0x06이 0x63 미만일 때만 motion accumulation을 수행한다."},
    {"slotField": "+0x0c/+0x0a", "actorField": "+0x0c/+0x0a, then +0x08=+0x0a", "source": "+0xcc + rand(+0xcd)", "meaning": "표시 좌표/offset 계열 1. 같은 delta가 slot과 actor에 누적된다."},
    {"slotField": "+0x12/+0x10", "actorField": "+0x12/+0x10, then +0x0e=+0x10", "source": "+0xce + rand(+0xcf)", "meaning": "표시 좌표/offset 계열 2. 같은 delta가 slot과 actor에 누적된다."},
    {"slotField": "+0xbe", "actorField": "+0x1c via 0x422b95", "source": "+0xd0 + rand(+0xd1)", "meaning": "표시 보정값 1. frame table 0x48b1dc의 slot +0x48/+0x49 보정과 합산 뒤 0 이상으로 clamp된다."},
    {"slotField": "+0xc2", "actorField": "+0x20 via 0x422b95", "source": "+0xd2 + rand(+0xd3)", "meaning": "표시 보정값 2. 0x422b95에서 frame table 보정과 합산된다."},
    {"slotField": "+0xc4", "actorField": "+0x22 via 0x422b95", "source": "+0xd4 + rand(+0xd5)", "meaning": "표시 보정값 3. 0x422b95에서 frame table 보정과 합산된다."},
    {"slotField": "+0xc6", "actorField": "+0x24 via 0x422b95", "source": "+0xd6 + rand(+0xd7)", "meaning": "표시 보정값 4. 0x422b95에서 frame table 보정과 합산된다."},
    {"slotField": "+0xc8", "actorField": "+0x26 via 0x422b95", "source": "rand(100) + slotAge/2 + 1", "meaning": "age에 비례해 커지는 randomized 표시 보정값. damage number/shake/flash 계열 후보로 보되 이름은 아직 미확정이다."},
    {"slotField": "slot +0x48/+0x49", "actorField": "+0x1c/+0x20/+0x22/+0x24/+0x26", "source": "0x48b1dc frame/effect table", "meaning": "0x422b95가 두 frame/effect entry의 +0x10..+0x14 signed offsets를 slot 보정값에 더한다."},
]


BIT_ROWS = [
    {"bit": "+0x62 bit 0x80", "source": "0x40e0fd test, 0x40d57f/0x40d61d OR", "meaning": "display wait skip/completed bit. wait loop가 이 actor를 0x42177f 갱신 대상에서 제외한다.", "confidence": "확정"},
    {"bit": "+0x62 bit 0x40", "source": "0x40d532 mask 0xc0 compare 0x40, 0x40e1d8 AND 0xbf", "meaning": "phase 4 전환 전 상태. 0x40이면 phase 4/+0x5c=0x20 예약 후 0x80을 켠다. reset path에서는 이 bit를 지운다.", "confidence": "강한 추정"},
    {"bit": "+0x62 bit 0x02", "source": "0x434fa9 OR 0x02, 0x4351d1 AND ~0x02", "meaning": "timed command/action lock. +0x64/+0x66과 같이 쓰이며 display wait skip bit는 아니다. UI 상태명은 +0x2a가 담당한다.", "confidence": "확정"},
    {"bit": "+0x62 bit 0x01", "source": "0x40d9e8 AND ~0x01, 0x43505c OR 0x01", "meaning": "one-turn action/display latch. 0x40d95b battle-order cleanup에서 +0x2a를 복원한 뒤 지워진다. UI 상태명은 +0x2a가 담당한다.", "confidence": "확정"},
]


EVIDENCE_SPECS = [
    {
        "label": "wait loop skips +0x62 bit 0x80 actors and ORs 0x42177f return",
        "range": (0x0040E0C2, 0x0040E150),
        "needles": ["0x4576e8", "[eax+0x62]", "test   cl,0x80", "0x42177f", "or     DWORD PTR [ebp-0x4],ecx"],
    },
    {
        "label": "0x42177f maps actor to slot, accumulates motion, then returns AL=1",
        "range": (0x0042177F, 0x00421B33),
        "needles": ["0x4576e9", "0x457750", "cmp    ecx,0x63", "0x427730", "0x422b95", "mov    al,0x1"],
    },
    {
        "label": "0x422b95 folds slot randomized offsets with frame/effect table 0x48b1dc",
        "range": (0x00422B95, 0x00422D74),
        "needles": ["0x48b1dc", "[eax+0x10]", "[eax+0x11]", "[eax+0x12]", "[eax+0x13]", "[eax+0x14]", "[ecx+0x26]"],
    },
    {
        "label": "phase 4 path schedules display phase and sets +0x62 bit 0x80",
        "range": (0x0040D520, 0x0040D640),
        "needles": ["and    ecx,0xc0", "[eax+0x5b]", "[eax+0x5c]", "[eax+0x60]", "or     cl,0x80"],
    },
    {
        "label": "script byte can OR arbitrary +0x62 bits into actor sets",
        "range": (0x0040DB8C, 0x0040DC63),
        "needles": ["0x4576e8", "0x59db28", "[eax+0x62]", "or     cl,BYTE PTR [eax+0x2]"],
    },
    {
        "label": "local visual/status bits 0x01/0x02 are separate from wait skip 0x80",
        "range": (0x00434F90, 0x00435290),
        "needles": ["or     cl,0x2", "and    edx,0xfffffffd", "or     cl,0x1", "[eax+0x64]", "[eax+0x66]"],
    },
]


def build_evidence() -> list[dict[str, Any]]:
    rows = []
    for spec in EVIDENCE_SPECS:
        start, stop = spec["range"]
        rows.append({
            "label": spec["label"],
            "startVa": f"0x{start:08x}",
            "stopVa": f"0x{stop:08x}",
            "lines": compact_lines(disassemble(start, stop), spec["needles"]),
        })
    return rows


def make_table(headers: list[str], rows: list[dict[str, Any]], keys: list[str]) -> str:
    body = []
    for row in rows:
        body.append("<tr>" + "".join(f"<td>{esc(row.get(key, ''))}</td>" for key in keys) + "</tr>")
    return (
        "<table><thead><tr>"
        + "".join(f"<th>{esc(header)}</th>" for header in headers)
        + "</tr></thead><tbody>"
        + "\n".join(body)
        + "</tbody></table>"
    )


def make_html(data: dict[str, Any]) -> str:
    evidence_blocks = []
    for ev in data["evidence"]:
        evidence_blocks.append(
            "<details open>"
            f"<summary>{esc(ev['label'])} <code>{esc(ev['startVa'])}..{esc(ev['stopVa'])}</code></summary>"
            f"<pre>{esc(chr(10).join(ev['lines']))}</pre>"
            "</details>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Battle Actor Display Wait Semantics Review</title>
<style>
  body {{ margin: 24px; font-family: system-ui, -apple-system, Segoe UI, sans-serif; color: #1f2937; background: #f8fafc; }}
  h1, h2 {{ margin: 0 0 12px; }}
  section {{ margin: 22px 0; }}
  table {{ width: 100%; border-collapse: collapse; background: white; border: 1px solid #d8dee9; }}
  th, td {{ border: 1px solid #d8dee9; 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; }}
  pre {{ overflow: auto; padding: 12px; background: #111827; color: #e5e7eb; border-radius: 6px; font-size: 12px; line-height: 1.45; }}
  details {{ margin: 10px 0; background: white; border: 1px solid #d8dee9; border-radius: 6px; padding: 8px 10px; }}
  summary {{ cursor: pointer; font-weight: 700; }}
  .note {{ background: #fff7ed; border: 1px solid #fed7aa; padding: 12px; border-radius: 6px; }}
</style>
</head>
<body>
<h1>Battle Actor Display Wait Semantics Review</h1>
<p class="note">핵심 수정: <code>0x42177f</code>는 자체 완료 시 0을 반환하는 함수가 아니라, <code>+0x62 bit 0x80</code>으로 스킵되지 않은 actor slot을 갱신하면 <code>AL=1</code>을 반환한다. wait loop의 종료 조건은 모든 actor가 0x80 스킵 상태가 되어 OR accumulator가 0으로 남는 쪽이다.</p>

<section><h2>Wait Semantics</h2>{make_table(["Subject", "Meaning", "Confidence"], data["wait"], ["subject", "meaning", "confidence"])}</section>
<section><h2>Slot Update Fields</h2>{make_table(["Slot Field", "Actor Field", "Source", "Meaning"], data["slotFields"], ["slotField", "actorField", "source", "meaning"])}</section>
<section><h2>Relevant +0x62 Bits</h2>{make_table(["Bit", "Source", "Meaning", "Confidence"], data["bits"], ["bit", "source", "meaning", "confidence"])}</section>
<section><h2>Evidence</h2>{''.join(evidence_blocks)}</section>
</body>
</html>
"""


def make_md(data: dict[str, Any]) -> str:
    lines = [
        "# Battle Actor Display Wait Semantics Review",
        "",
        "## 결론",
        "",
        "`0x42177f`는 자체 완료 시 0을 반환하는 함수가 아니라, `+0x62 bit 0x80`으로 스킵되지 않은 actor slot을 갱신하면 `AL=1`을 반환한다.",
        "따라서 wait loop의 종료 조건은 모든 actor가 0x80 스킵 상태가 되어 OR accumulator가 0으로 남는 쪽이다.",
        "",
        "## Wait semantics",
    ]
    for row in data["wait"]:
        lines.append(f"- {row['subject']}: {row['meaning']} ({row['confidence']})")
    lines.extend(["", "## Slot update fields"])
    for row in data["slotFields"]:
        lines.append(f"- `{row['slotField']}` -> `{row['actorField']}`: {row['source']} / {row['meaning']}")
    lines.extend(["", "## +0x62 bits"])
    for row in data["bits"]:
        lines.append(f"- `{row['bit']}`: {row['meaning']} ({row['source']}, {row['confidence']})")
    return "\n".join(lines) + "\n"


def main() -> None:
    OUT.mkdir(exist_ok=True)
    data = {
        "version": 1,
        "kind": "hwanse-battle-actor-display-wait-semantics-review",
        "source": "Hwanse2.exe",
        "status": "actor-display-wait-loop-grounded",
        "summary": {
            "waitRows": len(WAIT_ROWS),
            "slotFieldRows": len(SLOT_FIELD_ROWS),
            "bitRows": len(BIT_ROWS),
            "evidenceBlocks": len(EVIDENCE_SPECS),
        },
        "wait": WAIT_ROWS,
        "slotFields": SLOT_FIELD_ROWS,
        "bits": BIT_ROWS,
        "evidence": build_evidence(),
    }
    (OUT / "battle_actor_display_wait_semantics_review.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    (OUT / "battle_actor_display_wait_semantics_review.html").write_text(make_html(data), encoding="utf-8")
    (OUT / "battle_actor_display_wait_semantics_review.md").write_text(make_md(data), encoding="utf-8")


if __name__ == "__main__":
    main()
