#!/usr/bin/env python3
"""Document the battle actor display slot lifecycle.

The previous display-state report promoted actor +0x5b/+0x5c/+0x60 as real
battle display VM state.  The remaining question was why actor +0x5b has many
"set to 1" sites but no obvious direct "set to 0" site.

This report narrows that gap: +0x5b schedules a display phase, while the active
display lifecycle is tracked through the global actor/display slot map.  The
wait loop polls those slots through 0x42177f, and another routine removes slots
by compacting the global map rather than clearing actor +0x5b directly.
"""
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):
            lo = max(0, index - context)
            hi = min(len(lines), index + context + 1)
            for i in range(lo, hi):
                selected.append((i, lines[i]))

    seen: set[int] = set()
    out: list[str] = []
    for index, line in selected:
        if index in seen:
            continue
        seen.add(index)
        out.append(line)
    return out


def full_disasm_field_refs() -> list[str]:
    result = subprocess.run(
        ["objdump", "-Mintel", "-D", "-b", "pei-i386", str(EXE)],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    return [
        line
        for line in result.stdout.splitlines()
        if "+0x5b]" in line and re.match(r"\s*[0-9a-f]{6,8}:", line)
    ]


def scan_zero_write_patterns() -> list[dict[str, Any]]:
    data = EXE.read_bytes()
    patterns = [
        ("mov byte [eax+0x5b], 0", bytes.fromhex("c6 40 5b 00")),
        ("mov byte [ecx+0x5b], 0", bytes.fromhex("c6 41 5b 00")),
        ("mov byte [edx+0x5b], 0", bytes.fromhex("c6 42 5b 00")),
        ("mov byte [ebx+0x5b], 0", bytes.fromhex("c6 43 5b 00")),
        ("mov byte [esi+0x5b], 0", bytes.fromhex("c6 46 5b 00")),
        ("mov byte [edi+0x5b], 0", bytes.fromhex("c6 47 5b 00")),
        ("mov byte [eax+0x0000005b], 0", bytes.fromhex("c6 80 5b 00 00 00 00")),
        ("mov byte [ecx+0x0000005b], 0", bytes.fromhex("c6 81 5b 00 00 00 00")),
        ("mov byte [edx+0x0000005b], 0", bytes.fromhex("c6 82 5b 00 00 00 00")),
        ("mov byte [ebx+0x0000005b], 0", bytes.fromhex("c6 83 5b 00 00 00 00")),
        ("mov byte [esi+0x0000005b], 0", bytes.fromhex("c6 86 5b 00 00 00 00")),
        ("mov byte [edi+0x0000005b], 0", bytes.fromhex("c6 87 5b 00 00 00 00")),
    ]
    rows = []
    for label, pattern in patterns:
        offsets: list[str] = []
        start = 0
        while True:
            found = data.find(pattern, start)
            if found < 0:
                break
            offsets.append(f"0x{found:06x}")
            start = found + 1
        rows.append({"pattern": label, "hits": offsets, "hitCount": len(offsets)})
    return rows


GLOBAL_ROWS = [
    {
        "address": "0x4576e8",
        "name": "active actor/display slot count",
        "meaning": "현재 전투 표시 계층에서 순회하는 actor/display slot 개수. 여러 루프의 상한으로 사용된다.",
        "confidence": "확정",
    },
    {
        "address": "0x4576e9[index]",
        "name": "slot index -> actor index map",
        "meaning": "display slot 순번을 actor index로 변환하는 byte table. 0x43215c가 추가하고 0x43264c가 압축한다.",
        "confidence": "확정",
    },
    {
        "address": "0x457750 + slotId * 216",
        "name": "display slot record",
        "meaning": "0x42177f가 매 프레임 갱신하는 표시용 slot record. slotId 계산식은 (id*3*8)*9라서 216 bytes 단위다.",
        "confidence": "확정",
    },
    {
        "address": "0x59db30[index]",
        "name": "active actor/display pointer array",
        "meaning": "slot 순회 루프와 제거 루틴이 사용하는 pointer array. 제거 시 뒤 항목을 당기고 마지막을 0으로 지운다.",
        "confidence": "확정",
    },
]


FLOW_ROWS = [
    {
        "step": "1. phase schedule",
        "va": "0x40cec2 / 0x40d033 / 0x40d2f0 / 0x40d54d / 0x40d600 / 0x40d7a6 / 0x40dd5e / 0x40dda4 / 0x40e1fa",
        "meaning": "전투 VM opcode가 actor +0x5c/+0x60을 설정하고 actor +0x5b=1로 표시 phase를 예약한다.",
        "confidence": "확정",
    },
    {
        "step": "2. slot attach",
        "va": "0x43215c..0x43218f",
        "meaning": "actor index를 0x4576e9에 추가하고 0x457750 slot 주소를 0x59db30에 넣은 뒤 0x4576e8 count를 증가시킨다.",
        "confidence": "확정",
    },
    {
        "step": "3. display wait",
        "va": "0x40e0c2..0x40e14f",
        "meaning": "active actor들을 돌며 +0x62 bit 0x80이 꺼진 actor만 0x42177f(actorIndex)로 갱신한다. 하나라도 갱신되면 script branch/wait로 간다.",
        "confidence": "확정",
    },
    {
        "step": "4. per-frame slot update",
        "va": "0x42177f..0x421ac2",
        "meaning": "0x4576e9로 actor의 display slot을 찾고, slot motion/timer/randomized curve 값을 누적한 뒤 actor 표시 필드에 반영한다.",
        "confidence": "확정",
    },
    {
        "step": "5. reset phase request",
        "va": "0x40e190..0x40e216",
        "meaning": "actor +0x62 bit 0x40을 지우고 actor +0x5b=1, +0x60=0으로 return-to-base/reset 표시 phase를 다시 예약한다.",
        "confidence": "강한 추정",
    },
    {
        "step": "6. slot remove/compact",
        "va": "0x43264c..0x4326e7",
        "meaning": "대상 actor index를 0x4576e9에서 찾아 뒤 slot과 pointer를 앞으로 당기고 0x4576e8 count를 감소시킨다.",
        "confidence": "확정",
    },
]


EVIDENCE_SPECS = [
    {
        "label": "display wait loop calls 0x42177f for active actors",
        "range": (0x0040E0C2, 0x0040E150),
        "needles": ["0x42177f", "0x4576e8", "0x4576e9", "[eax+0x4]", "[eax+0x8]"],
        "context": 4,
    },
    {
        "label": "reset/return-to-base schedules +0x5b=1 and +0x60=0",
        "range": (0x0040E190, 0x0040E216),
        "needles": ["and    cl,0xbf", "[eax+0x5b]", "[eax+0x60]"],
        "context": 4,
    },
    {
        "label": "0x42177f maps actor index to display slot and updates slot record",
        "range": (0x0042177F, 0x00421B30),
        "needles": ["0x4576e9", "0x457750", "0x427730", "0x422b95", "[eax+0x6]", "[edx+0xc]", "[eax+0x1c]"],
        "context": 3,
    },
    {
        "label": "slot attach: append actor index, slot record pointer, increment count",
        "range": (0x00432150, 0x004321A0),
        "needles": ["0x4576e8", "0x4576e9", "0x457750", "0x59db30"],
        "context": 4,
    },
    {
        "label": "slot remove: compact 0x4576e9 and 0x59db30, decrement count",
        "range": (0x0043264C, 0x004326F0),
        "needles": ["0x4576e9", "0x59db30", "0x4576e8"],
        "context": 4,
    },
    {
        "label": "actor local visual timer clears +0x62 bit 0x02, not +0x5b",
        "range": (0x0043512E, 0x00435290),
        "needles": ["[eax+0x64]", "[eax+0x66]", "and    edx,0xfffffffd", "[eax+0x62]"],
        "context": 3,
    },
]


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


def classify_refs(refs: list[str]) -> dict[str, list[str]]:
    set_to_one = [line for line in refs if ",0x1" in line and "mov" in line]
    set_to_zero = [line for line in refs if ",0x0" in line and "mov" in line]
    other = [line for line in refs if line not in set_to_one and line not in set_to_zero]
    return {"setToOne": set_to_one, "setToZero": set_to_zero, "otherRefs": other}


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>"
        )

    refs = data["fieldRefs"]
    pattern_rows = [
        {"pattern": row["pattern"], "hitCount": row["hitCount"], "hits": ", ".join(row["hits"]) or "-"}
        for row in data["zeroWritePatternScan"]
    ]

    return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Battle Actor Display Slot Lifecycle 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; }}
  .tag {{ display: inline-block; border-radius: 999px; padding: 2px 8px; background: #e5e7eb; font-size: 12px; white-space: nowrap; }}
  .확정 {{ background: #dcfce7; color: #166534; }}
  .강한\\ 추정 {{ background: #fef3c7; color: #92400e; }}
  .note {{ background: #fff7ed; border: 1px solid #fed7aa; padding: 12px; border-radius: 6px; }}
  small {{ color: #64748b; }}
</style>
</head>
<body>
<h1>Battle Actor Display Slot Lifecycle Review</h1>
<p class="note">결론: <code>actor +0x5b</code>는 display phase activation latch다. 직접 <code>0</code>을 쓰는 clear 명령은 발견되지 않았고, 실제 active/wait/completion 생명주기는 <code>0x4576e8</code>, <code>0x4576e9</code>, <code>0x457750</code>, <code>0x59db30</code>으로 구성된 display slot map/count에서 관리된다.</p>

<section>
<h2>Lifecycle Summary</h2>
{make_table(["Step", "VA", "Meaning", "Confidence"], data["flow"], ["step", "va", "meaning", "confidence"])}
</section>

<section>
<h2>Global Slot Structures</h2>
{make_table(["Address", "Name", "Meaning", "Confidence"], data["globals"], ["address", "name", "meaning", "confidence"])}
</section>

<section>
<h2>actor +0x5b Reference Scan</h2>
<p><code>+0x5b</code> 전체 참조 중 즉시값 <code>1</code> write는 {len(refs["setToOne"])}개, 즉시값 <code>0</code> write는 {len(refs["setToZero"])}개다.</p>
<details open><summary>set to 1 references</summary><pre>{esc(chr(10).join(refs["setToOne"]))}</pre></details>
<details><summary>other +0x5b references</summary><pre>{esc(chr(10).join(refs["otherRefs"]) or "-")}</pre></details>
</section>

<section>
<h2>Immediate Zero Write Pattern Scan</h2>
<p>일반 register 기반 <code>mov byte [reg+0x5b], 0</code> 패턴도 별도로 파일 바이트에서 확인했다. 모두 hit 0이다.</p>
{make_table(["Pattern", "Hit Count", "File Offsets"], pattern_rows, ["pattern", "hitCount", "hits"])}
</section>

<section>
<h2>Interpretation</h2>
<ul>
  <li><code>+0x5b=1</code>은 표시 phase를 예약하는 latch이며, phase 내용은 주로 <code>+0x60</code>과 <code>+0x5c</code>가 결정한다.</li>
  <li>대기 여부는 <code>+0x62 bit 0x80</code>으로 스킵되지 않은 actor를 <code>0x42177f</code>가 갱신했는지 <code>0x40e0c2</code> wait loop가 OR 집계해서 판단한다. 세부는 <a href="battle_actor_display_wait_semantics_review.html">display wait semantics</a> 보고서로 분리했다.</li>
  <li>표시 actor의 종료/제거는 <code>0x43264c</code> 계열이 global slot map을 압축하는 방식으로 처리한다. 따라서 <code>actor +0x5b = 0</code> clear 명령을 찾으려 하면 구조를 잘못 좁히게 된다.</li>
  <li><code>0x40e190</code>은 clear가 아니라 reset 표시 phase를 다시 예약한다. 이 경로에서 <code>+0x60=0</code>이 들어가는 이유가 return-to-base phase로 해석된다.</li>
</ul>
</section>

<section>
<h2>Evidence</h2>
{''.join(evidence_blocks)}
</section>
</body>
</html>
"""


def make_md(data: dict[str, Any]) -> str:
    refs = data["fieldRefs"]
    lines = [
        "# Battle Actor Display Slot Lifecycle Review",
        "",
        "## 결론",
        "",
        "`actor +0x5b`는 display phase activation latch다. 직접 `0`을 쓰는 clear 명령은 발견되지 않았다.",
        "실제 active/wait/completion 생명주기는 `0x4576e8`, `0x4576e9`, `0x457750`, `0x59db30` slot map/count에서 관리된다.",
        "",
        "## 흐름",
    ]
    for row in data["flow"]:
        lines.append(f"- {row['step']} `{row['va']}`: {row['meaning']} ({row['confidence']})")
    lines.extend(["", "## Global slot 구조"])
    for row in data["globals"]:
        lines.append(f"- `{row['address']}`: {row['name']} - {row['meaning']} ({row['confidence']})")
    lines.extend([
        "",
        "## actor +0x5b scan",
        f"- set-to-1 references: {len(refs['setToOne'])}",
        f"- set-to-0 references: {len(refs['setToZero'])}",
        "- immediate-zero byte-pattern hits: 0",
        "",
        "## 해석",
        "- `+0x5b=1`은 표시 phase 예약 latch이며, phase 내용은 `+0x60`과 `+0x5c`가 결정한다.",
        "- 대기 여부는 `+0x62 bit 0x80`으로 스킵되지 않은 actor를 `0x42177f`가 갱신했는지 `0x40e0c2` wait loop가 OR 집계해서 판단한다. 세부는 `battle_actor_display_wait_semantics_review`로 분리했다.",
        "- 표시 actor 종료/제거는 `0x43264c` 계열이 global slot map을 압축하는 방식으로 처리한다.",
        "- `0x40e190`은 clear가 아니라 `+0x60=0` return-to-base/reset 표시 phase를 다시 예약한다.",
    ])
    return "\n".join(lines) + "\n"


def main() -> None:
    OUT.mkdir(exist_ok=True)
    refs = classify_refs(full_disasm_field_refs())
    data = {
        "flow": FLOW_ROWS,
        "globals": GLOBAL_ROWS,
        "fieldRefs": refs,
        "zeroWritePatternScan": scan_zero_write_patterns(),
        "evidence": build_evidence(),
    }
    (OUT / "battle_actor_display_slot_lifecycle_review.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    (OUT / "battle_actor_display_slot_lifecycle_review.html").write_text(make_html(data), encoding="utf-8")
    (OUT / "battle_actor_display_slot_lifecycle_review.md").write_text(make_md(data), encoding="utf-8")


if __name__ == "__main__":
    main()
