#!/usr/bin/env python3
"""Document battle actor display-state fields.

The battle actor record has a small cluster of fields around +0x5b/+0x5c/+0x60
that kept showing up while tracing result families and action display scripts.
This report keeps those fields separate from the damage formula:

* +0x5b: activation latch for an actor display phase
* +0x5c: result/display participation bitmask
* +0x60: display phase selector
* 0x59e334: global branch state used by the same script layer

The goal is not to name every phase perfectly yet, but to remove the old
ambiguity that these bytes might be arbitrary payload leftovers.
"""
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 = 2) -> 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


FIELD_ROWS = [
    {
        "field": "actor +0x5b",
        "name": "display-phase activation latch",
        "confidence": "확정",
        "meaning": (
            "전투 스크립트가 actor +0x5c/+0x60을 설정한 뒤 이 값을 1로 세운다. "
            "이후 actor display/update layer가 해당 actor의 표시 단계를 실행해야 함을 뜻한다."
        ),
        "evidence": [
            "0x40d033: skill/action phase setup after +0x60 selection.",
            "0x40d2f0: pending +0x5c actor re-activation.",
            "0x40d54d, 0x40d600, 0x40d7a6, 0x40dd5e: result/common flow phase scheduling.",
            "0x40e1fa: reset/return-to-base phase scheduling.",
        ],
    },
    {
        "field": "actor +0x5c",
        "name": "result/display participation bitmask",
        "confidence": "확정",
        "meaning": (
            "단일 값이 아니라 비트마스크다. result apply/test layer가 0xfbff 마스크로 검사하고, "
            "여러 VM opcode가 OR/AND로 비트를 세우거나 지운다. 0x0400은 일부 대기 검사에서 무시된다."
        ),
        "evidence": [
            "0x40d1b4, 0x40d2d9, 0x40eb3a, 0x40eb9d: test [actor+0x5c], 0xfbff.",
            "0x40d283: OR 0x3 for current actor.",
            "0x40da3e/0x40da94: AND NOT script mask.",
            "0x40dacc/0x40daf5: OR script mask.",
            "0x4117xx and many helper scripts OR 0x2/0x100/0x200/0x400.",
        ],
    },
    {
        "field": "actor +0x60",
        "name": "display phase selector",
        "confidence": "확정",
        "meaning": (
            "스킬 id 자체가 아니라 표시 단계 selector다. 일반 기술에서는 skillId + 0x0a로 계산되지만, "
            "결과 흐름/공용 상태에서는 0/3/4/5/6/7/8 같은 phase가 직접 들어간다."
        ),
        "evidence": [
            "0x40cf6e: player actor +0x60 = actor+0x59(skillId) + 0x0a.",
            "0x40cfc7: type-2 actor +0x60 = actor+0x5a + 0x0a.",
            "0x40cf79: alternate player phase 7.",
            "0x40ceb1, 0x40dad9, 0x40d56c, 0x40d7c5, 0x40dd7d, 0x40e201: direct common phases.",
        ],
    },
    {
        "field": "global 0x59e334",
        "name": "battle display branch state",
        "confidence": "강한 추정",
        "meaning": (
            "도주/전환 흐름에서 0/1/2로 세팅되고, 별도 VM opcode가 script byte와 비교해 branch한다. "
            "전투 전체 흐름이 어떤 결과 분기로 넘어갔는지 표시하는 전역 상태로 보인다."
        ),
        "evidence": [
            "0x40d6f2: all primary actors have +0x62 bit 0x80 -> 0x59e334=1.",
            "0x40d834: transition completion path -> 0x59e334=0.",
            "0x40dd86/0x40ddb9: family-1 escape fan-out -> 0x59e334=2.",
            "0x40e163: VM branch compares script byte with 0x59e334.",
        ],
    },
]


PHASE_ROWS = [
    {"phase": "skillId + 0x0a", "source": "0x40cf60..0x40cf6e", "meaning": "player actor normal skill/action display phase", "confidence": "확정"},
    {"phase": "actor+0x5a + 0x0a", "source": "0x40cfb9..0x40cfc7", "meaning": "type-2/shared/enemy action display phase", "confidence": "확정"},
    {"phase": "0", "source": "0x40e201", "meaning": "reset/return-to-base display phase after clearing +0x62 bit 0x40", "confidence": "강한 추정"},
    {"phase": "3", "source": "0x40dad9", "meaning": "script mask fan-out display/result phase", "confidence": "강한 추정"},
    {"phase": "4", "source": "0x40d56c, 0x40d611", "meaning": "actors with +0x62 high bits == 0x40 get mask 0x20 and phase 4", "confidence": "강한 추정"},
    {"phase": "5", "source": "0x40d7c5", "meaning": "actors not yet marked +0x80 get mask 0x10 and phase 5 during all-actor transition", "confidence": "강한 추정"},
    {"phase": "6", "source": "0x40dd7d, 0x40ddb5", "meaning": "family-1/도주 fan-out phase with mask 0x3", "confidence": "확정"},
    {"phase": "7", "source": "0x40cf79", "meaning": "alternate player actor display phase when actor+0x58 == 1", "confidence": "강한 추정"},
    {"phase": "8", "source": "0x40ceb1", "meaning": "pre/action state setup phase after 0x4336f1/0x433932/0x43376e path", "confidence": "강한 추정"},
]


MASK_ROWS = [
    {"mask": "0x00000003", "source": "0x40d283, 0x40dd6c", "meaning": "common immediate result/display mask; also used by family-1 escape fan-out", "confidence": "확정"},
    {"mask": "0x00000010", "source": "0x40d7b4", "meaning": "all-actor transition mask paired with phase 5", "confidence": "강한 추정"},
    {"mask": "0x00000020", "source": "0x40d55b, 0x40d607", "meaning": "actor +0x62 high-state transition mask paired with phase 4", "confidence": "강한 추정"},
    {"mask": "0x00000100", "source": "0x411b49, 0x411c56", "meaning": "helper/display script participation bit", "confidence": "근거확보"},
    {"mask": "0x00000200", "source": "0x40e915", "meaning": "motion/helper script participation bit", "confidence": "근거확보"},
    {"mask": "0x00000400", "source": "0x414f75, 0x41507e and 0xfbff tests", "meaning": "special bit ignored by 0xfbff pending-result tests", "confidence": "근거확보"},
    {"mask": "0x0000fbff", "source": "0x40d1b4, 0x40d2d9, 0x40eb3a, 0x40eb9d", "meaning": "pending-result test mask; excludes 0x0400", "confidence": "확정"},
]


EVIDENCE_SPECS = [
    {
        "label": "normal action setup: +0x60 phase, +0x5b latch",
        "range": (0x0040CF0A, 0x0040D04B),
        "needles": ["[eax+0x60]", "[eax+0x5b]", "0x59e2a4", "0x7c", "0x7d"],
        "context": 3,
    },
    {
        "label": "result apply/test layer: +0x5c pending mask",
        "range": (0x0040D080, 0x0040D32C),
        "needles": ["[eax+0x5c]", "0xfbff", "0x4340ae", "[eax+0x5b]", "0x3"],
        "context": 3,
    },
    {
        "label": "wait until selected/all +0x5c masks clear",
        "range": (0x0040D32D, 0x0040D4DD),
        "needles": ["[eax+0x5c]", "0xfbbf", "0xfbff", "0x55a1b8"],
        "context": 3,
    },
    {
        "label": "actor +0x62 high-state transition -> phase 4",
        "range": (0x0040D4DD, 0x0040D698),
        "needles": ["0xc0", "0x40", "[eax+0x5b]", "[eax+0x5c]", "[eax+0x60]", "0x80"],
        "context": 3,
    },
    {
        "label": "all-actor transition -> phase 5 and 0x59e334 state",
        "range": (0x0040D698, 0x0040D856),
        "needles": ["0x59e334", "[eax+0x62]", "[eax+0x5b]", "[eax+0x5c]", "[eax+0x60]", "0x457734"],
        "context": 3,
    },
    {
        "label": "script OR/AND mask operations on +0x5c",
        "range": (0x0040DA05, 0x0040DB36),
        "needles": ["[ecx+0x5c]", "[eax+0x5c]", "[eax+0x60]", "0x3"],
        "context": 3,
    },
    {
        "label": "family-1/도주 flow fan-out -> phase 6",
        "range": (0x0040DCF7, 0x0040DDFD),
        "needles": ["test   cl,0x4", "[eax+0x5b]", "[eax+0x5c]", "[eax+0x60]", "0x59e334"],
        "context": 3,
    },
    {
        "label": "0x59e334 branch consumer",
        "range": (0x0040E150, 0x0040E190),
        "needles": ["0x59e334", "cmp", "[eax+0x4]"],
        "context": 4,
    },
    {
        "label": "reset/return-to-base phase 0",
        "range": (0x0040E190, 0x0040E216),
        "needles": ["and    cl,0xbf", "[eax+0x5b]", "[eax+0x60]"],
        "context": 4,
    },
    {
        "label": "helper/motion path ORs +0x5c bit 0x200",
        "range": (0x0040E8FF, 0x0040EAA7),
        "needles": ["[eax+0x5c]", "0x200", "0x402321", "0x435b5b"],
        "context": 3,
    },
    {
        "label": "pending +0x5c can hold script flow",
        "range": (0x0040EAD1, 0x0040EBD0),
        "needles": ["0xfbff", "and    DWORD PTR [eax+0x5c],0xfffc", "0x55a1b8"],
        "context": 3,
    },
]


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


def make_html(data: dict[str, Any]) -> str:
    def 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>"
        )

    field_rows = []
    for row in data["fields"]:
        field_rows.append(
            "<tr>"
            f"<td><code>{esc(row['field'])}</code></td>"
            f"<td>{esc(row['name'])}</td>"
            f"<td><span class='tag {esc(row['confidence'])}'>{esc(row['confidence'])}</span></td>"
            f"<td>{esc(row['meaning'])}<br><small>{esc(' / '.join(row['evidence']))}</small></td>"
            "</tr>"
        )

    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 State 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; }}
  .근거확보 {{ background: #dbeafe; color: #1e40af; }}
  small {{ color: #64748b; }}
  .note {{ background: #fff7ed; border: 1px solid #fed7aa; padding: 12px; border-radius: 6px; }}
</style>
</head>
<body>
<h1>Battle Actor Display State Review</h1>
<p class="note">이 문서는 actor <code>+0x5b/+0x5c/+0x60</code>와 전역 <code>0x59e334</code>를 전투 결과/표시 VM 상태값으로 승격한 근거를 정리한다. 아직 phase 이름은 전부 확정하지 않았지만, 각 필드의 역할은 더 이상 payload-only 후보로 두지 않는다.</p>

<section>
<h2>Field Semantics</h2>
<table><thead><tr><th>Field</th><th>Name</th><th>Confidence</th><th>Meaning / Evidence</th></tr></thead><tbody>
{''.join(field_rows)}
</tbody></table>
</section>

<section>
<h2>Known +0x60 Phases</h2>
{table(["Phase", "Source", "Meaning", "Confidence"], data["phases"], ["phase", "source", "meaning", "confidence"])}
</section>

<section>
<h2>Known +0x5c Masks</h2>
{table(["Mask", "Source", "Meaning", "Confidence"], data["masks"], ["mask", "source", "meaning", "confidence"])}
</section>

<section>
<h2>Remaining Gaps</h2>
<ul>
  <li><code>+0x60</code> phase 3/4/5/7/8의 정확한 화면 이름은 아직 런타임 대조가 필요하다.</li>
  <li><code>+0x5c</code> 0x100/0x200/0x400 계열은 helper/display script 참여 비트까지는 확정했지만, 각 비트의 사용자-visible 이름은 추가 추적 대상이다.</li>
  <li><code>+0x5b</code> clear처럼 보였던 생명주기는 별도 <a href="battle_actor_display_slot_lifecycle_review.html">display slot lifecycle</a> 보고서로 분리했다. 직접 0 write가 아니라 slot map/count로 관리된다.</li>
</ul>
</section>

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


def make_md(data: dict[str, Any]) -> str:
    lines = [
        "# Battle Actor Display State Review",
        "",
        "## 확정 요약",
    ]
    for row in data["fields"]:
        lines.append(f"- `{row['field']}`: {row['name']} ({row['confidence']}) - {row['meaning']}")
    lines.extend(["", "## +0x60 phase", ""])
    for row in data["phases"]:
        lines.append(f"- `{row['phase']}`: {row['meaning']} ({row['source']}, {row['confidence']})")
    lines.extend(["", "## +0x5c mask", ""])
    for row in data["masks"]:
        lines.append(f"- `{row['mask']}`: {row['meaning']} ({row['source']}, {row['confidence']})")
    lines.extend([
        "",
        "## 남은 구멍",
        "- `+0x60` phase 3/4/5/7/8의 사용자-visible 이름은 추가 런타임 대조가 필요하다.",
        "- `+0x5c` 0x100/0x200/0x400의 정확한 이름은 helper/display script 후속 추적 대상이다.",
        "- `+0x5b` clear처럼 보였던 생명주기는 `battle_actor_display_slot_lifecycle_review`로 분리했다. 직접 0 write가 아니라 slot map/count로 관리된다.",
    ])
    return "\n".join(lines) + "\n"


def main() -> None:
    OUT.mkdir(exist_ok=True)
    data = {
        "fields": FIELD_ROWS,
        "phases": PHASE_ROWS,
        "masks": MASK_ROWS,
        "evidence": build_evidence(),
    }
    (OUT / "battle_actor_display_state_review.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    (OUT / "battle_actor_display_state_review.html").write_text(make_html(data), encoding="utf-8")
    (OUT / "battle_actor_display_state_review.md").write_text(make_md(data), encoding="utf-8")


if __name__ == "__main__":
    main()
