#!/usr/bin/env python3
"""Build a focused review for actor +0x62 bits 0x40/0x80.

Bit 0x40 and bit 0x80 are easy to conflate because revive consumables clear
both by overwriting +0x62 with 0x01.  This report keeps their roles separate:
0x40 is the HP-zero/knockout state, while 0x80 is the inactive/display-wait
skip state reached after the knockout display transition.
"""
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 hex32(value: int) -> str:
    return f"0x{value:08x}"


def disassemble(start_va: int, stop_va: int) -> str:
    try:
        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,
        )
    except (OSError, subprocess.CalledProcessError) as exc:
        return f"; disassembly unavailable: {exc}"
    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 = 0) -> list[str]:
    lines = disasm.splitlines()
    picked: 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)):
                picked.append((i, lines[i]))
    seen: set[int] = set()
    result: list[str] = []
    for index, line in picked:
        if index not in seen:
            seen.add(index)
            result.append(line)
    return result


LIFECYCLE_ROWS = [
    {
        "stage": "HP damage apply",
        "va": 0x004353F8,
        "operation": "if current HP <= damage: +0x08=0; +0x62|=0x40; call 0x4354eb",
        "meaning": "HP가 0이 되는 순간 세워지는 실제 knockout flag는 0x40이다.",
        "confidence": "확정",
    },
    {
        "stage": "status mapper",
        "va": 0x00434E84,
        "operation": "if +0x62&0x40 and not +0x80: save +0x2a to +0x66, set +0x2a=0x1b, +0x64=0",
        "meaning": "0x40 actor를 기절 상태 id 0x1b로 매핑한다. 이미 0x80이면 이 mapper는 바로 빠진다.",
        "confidence": "확정",
    },
    {
        "stage": "phase 4 display scheduling",
        "va": 0x0040D543,
        "operation": "when (+0x62 & 0xc0)==0x40: +0x5b=1; +0x5c=0x20; +0x60=4; +0x62|=0x80",
        "meaning": "0x40만 있는 actor를 기절/비활성 표시 phase로 예약한 뒤 display-wait skip bit 0x80을 세운다.",
        "confidence": "확정",
    },
    {
        "stage": "current-order phase 4 scheduling",
        "va": 0x0040D5FD,
        "operation": "same phase-4 schedule for current order actor when (+0x62 & 0xc0)==0x40",
        "meaning": "전체 actor 루프와 현재 order actor 루프가 같은 전환 규칙을 쓴다.",
        "confidence": "확정",
    },
    {
        "stage": "display wait loop",
        "va": 0x0040E0C2,
        "operation": "skip actor if +0x62&0x80; otherwise call 0x42177f and OR returned AL",
        "meaning": "0x80 actor는 display wait 갱신 대상에서 제외된다. 모두 0x80이면 wait가 끝난다.",
        "confidence": "확정",
    },
    {
        "stage": "return/reset display pass",
        "va": 0x0040E190,
        "operation": "for battle-order actors: +0x62&=0xbf; if no 0x80 then +0x5b=1, +0x60=0",
        "meaning": "0x40만 지우고 0x80 actor는 base-return 표시 예약에서 제외한다.",
        "confidence": "고신뢰",
    },
    {
        "stage": "revive consumable prelude",
        "va": 0x0043583C,
        "operation": "if +0x62&0x80: restore +0x2a from +0x66, clear +0x66/+0x64, +0x62=0x01",
        "meaning": "리프레시 워터는 0x40/0x80을 둘 다 지우고 one-turn latch 0x01로 복귀시킨다.",
        "confidence": "확정",
    },
    {
        "stage": "beast stone prelude",
        "va": 0x0043590D,
        "operation": "same revive prelude, then HP/MP max recovery",
        "meaning": "마수석도 같은 방식으로 기절/비활성 actor를 active actor로 되돌린다.",
        "confidence": "확정",
    },
]


SKIP_ROWS = [
    {
        "va": 0x0040DB36,
        "site": "action-script current-order gate",
        "test": "test +0x62, 0x83",
        "meaning": "0x80 actor는 0x01/0x02 actor와 같이 normal action stream 대신 branch target으로 간다.",
    },
    {
        "va": 0x0040D95B,
        "site": "battle-order one-turn cleanup",
        "test": "skip actor if +0x62&0x80",
        "meaning": "비활성 actor는 0x01 cleanup 대상에서도 제외된다.",
    },
    {
        "va": 0x0040DD18,
        "site": "family-1 fan-out",
        "test": "skip actor if +0x62&0x80",
        "meaning": "도주/display fan-out도 이미 비활성인 actor를 건드리지 않는다.",
    },
    {
        "va": 0x00435295,
        "site": "HP recovery result clamp",
        "test": "if battle result context and +0x62&0x80, do not write visible +0x6e result",
        "meaning": "비활성 actor는 회복량 표시/result object 기록도 별도 처리된다.",
    },
]


RELATED_FIELDS = [
    ("actor+0x08", "current HP. damage apply sets it to 0 when knockout happens."),
    ("actor+0x0a", "max HP. player actor HP max can be reduced after knockout by a separate type-1 path in 0x4353f8."),
    ("actor+0x2a", "current mode/status id. knockout mapper writes 0x1b."),
    ("actor+0x5b", "display slot active/request flag."),
    ("actor+0x5c", "display mask. knockout transition uses 0x20."),
    ("actor+0x60", "display phase/state. knockout transition uses phase 4; return/reset path uses phase 0."),
    ("actor+0x62 bit 0x40", "HP-zero / knockout state."),
    ("actor+0x62 bit 0x80", "inactive/display-wait skip and target/order exclusion state."),
    ("actor+0x64", "status timer, cleared on knockout/revive transitions."),
    ("actor+0x66", "prior status/mode backup restored by revive."),
]


EVIDENCE_SPECS = [
    {
        "label": "HP zero sets +0x62 bit 0x40 and full reset preserves 0x40/0x80",
        "range": (0x004353F8, 0x00435538),
        "needles": ["[eax+0x8]", "or     cl,0x40", "call   0x4354eb", "[eax+0x66]", "and    cl,0xc0", "[eax+0x69],0x0"],
        "context": 2,
    },
    {
        "label": "status mapper turns 0x40 into +0x2a status 0x1b unless actor already has 0x80",
        "range": (0x00434E84, 0x00434F18),
        "needles": ["test   cl,0x80", "test   cl,0x40", "[ecx+0x66]", "[eax+0x2a],0x1b", "[eax+0x64],0x0", "[eax+0x68]"],
        "context": 2,
    },
    {
        "label": "phase 4 transition sets +0x80 after display scheduling",
        "range": (0x0040D520, 0x0040D680),
        "needles": ["and    ecx,0xc0", "[eax+0x5b],0x1", "[eax+0x5c],0x20", "[eax+0x60],0x4", "or     cl,0x80"],
        "context": 2,
    },
    {
        "label": "display wait skips +0x80 actors",
        "range": (0x0040E0C2, 0x0040E150),
        "needles": ["[eax+0x62]", "test   cl,0x80", "call   0x42177f", "or     DWORD PTR [ebp-0x4],ecx", "add    DWORD PTR [eax+0x40],0x8"],
        "context": 2,
    },
    {
        "label": "return/reset display pass clears 0x40 but skips 0x80 actors",
        "range": (0x0040E190, 0x0040E216),
        "needles": ["and    cl,0xbf", "test   cl,0x80", "[eax+0x5b],0x1", "[eax+0x60],0x0"],
        "context": 2,
    },
    {
        "label": "action gate treats +0x80 as a normal-action skip bit",
        "range": (0x0040DB36, 0x0040DB8C),
        "needles": ["[eax+0x62]", "test   cl,0x83", "[eax+0x4]", "add    DWORD PTR [eax+0x40],0x8"],
        "context": 2,
    },
    {
        "label": "family fan-out skips +0x80 actors",
        "range": (0x0040DCF7, 0x0040DDC0),
        "needles": ["test   cl,0x4", "test   cl,0x80", "[eax+0x5b],0x1", "[eax+0x5c],0x3", "[eax+0x60],0x6"],
        "context": 2,
    },
    {
        "label": "revive consumables clear +0x40/+0x80 by overwriting +0x62 with 0x01",
        "range": (0x0043583C, 0x00435992),
        "needles": ["test   cl,0x80", "[eax+0x66]", "[ecx+0x2a]", "[eax+0x64]", "[eax+0x62],0x1", "0x435295", "0x435356"],
        "context": 2,
    },
]


def build_data() -> dict[str, Any]:
    evidence = []
    for spec in EVIDENCE_SPECS:
        start_va, stop_va = spec["range"]
        disasm = disassemble(start_va, stop_va)
        evidence.append(
            {
                "label": spec["label"],
                "range": f"{hex32(start_va)}..{hex32(stop_va)}",
                "lines": compact_lines(disasm, spec["needles"], context=spec["context"]),
            }
        )
    return {
        "title": "Battle Knockout / Inactive Actor Flag Review",
        "subject": "actor +0x62 bits 0x40 and 0x80",
        "summary": [
            "actor +0x62 bit 0x40 is the HP-zero / knockout state.",
            "actor +0x62 bit 0x80 is not the HP-zero state itself; it is the inactive/display-wait skip state set after phase-4 knockout display scheduling.",
            "The display transition checks (+0x62 & 0xc0)==0x40, schedules +0x5b=1/+0x5c=0x20/+0x60=4, then ORs bit 0x80.",
            "0x80 actors are skipped by display wait, action gate, battle-order cleanup, and fan-out paths.",
            "Refresh water and beast stone revive only when 0x80 is present, then overwrite +0x62 with 0x01, clearing both 0x40 and 0x80.",
        ],
        "lifecycleRows": LIFECYCLE_ROWS,
        "skipRows": SKIP_ROWS,
        "relatedFields": RELATED_FIELDS,
        "evidence": evidence,
    }


def write_json(data: dict[str, Any]) -> None:
    (OUT / "battle_knockout_inactive_flag_review.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def write_md(data: dict[str, Any]) -> None:
    lines = [
        "# Battle Knockout / Inactive Actor Flag Review",
        "",
        "`actor +0x62 bit 0x40`은 HP 0/기절, `bit 0x80`은 그 뒤의 비활성/display-wait skip 상태다.",
        "",
        "## 결론",
        "",
    ]
    for item in data["summary"]:
        lines.append(f"- {item}")
    lines.extend(["", "## Lifecycle", ""])
    lines.append("| Stage | VA | Operation | Meaning | Confidence |")
    lines.append("|---|---|---|---|---|")
    for row in data["lifecycleRows"]:
        lines.append(
            f"| {row['stage']} | `{hex32(row['va'])}` | `{row['operation']}` | {row['meaning']} | {row['confidence']} |"
        )
    lines.extend(["", "## 0x80 Skip Sites", ""])
    lines.append("| VA | Site | Test | Meaning |")
    lines.append("|---|---|---|---|")
    for row in data["skipRows"]:
        lines.append(f"| `{hex32(row['va'])}` | {row['site']} | `{row['test']}` | {row['meaning']} |")
    lines.extend(["", "## Related Fields", ""])
    for field, meaning in data["relatedFields"]:
        lines.append(f"- `{field}`: {meaning}")
    lines.extend(["", "## Evidence", ""])
    for ev in data["evidence"]:
        lines.append(f"### {ev['label']} ({ev['range']})")
        lines.append("")
        lines.append("```asm")
        lines.extend(ev["lines"] or ["; no compact evidence lines matched"])
        lines.append("```")
        lines.append("")
    (OUT / "battle_knockout_inactive_flag_review.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")


def table(headers: list[str], rows: list[list[Any]]) -> str:
    chunks = ["<table><thead><tr>"]
    chunks.extend(f"<th>{esc(header)}</th>" for header in headers)
    chunks.append("</tr></thead><tbody>")
    for row in rows:
        chunks.append("<tr>")
        chunks.extend(f"<td>{cell}</td>" for cell in row)
        chunks.append("</tr>")
    chunks.append("</tbody></table>")
    return "".join(chunks)


def write_html(data: dict[str, Any]) -> None:
    lifecycle_rows = [
        [
            esc(row["stage"]),
            f"<code>{hex32(row['va'])}</code>",
            f"<code>{esc(row['operation'])}</code>",
            esc(row["meaning"]),
            f"<span class=\"tag good\">{esc(row['confidence'])}</span>",
        ]
        for row in data["lifecycleRows"]
    ]
    skip_rows = [
        [
            f"<code>{hex32(row['va'])}</code>",
            esc(row["site"]),
            f"<code>{esc(row['test'])}</code>",
            esc(row["meaning"]),
        ]
        for row in data["skipRows"]
    ]
    related_rows = [[f"<code>{esc(field)}</code>", esc(meaning)] for field, meaning in data["relatedFields"]]
    evidence_blocks = []
    for ev in data["evidence"]:
        evidence_blocks.append(
            "<section class=\"card\"><h2>"
            + esc(ev["label"])
            + f" <small>{esc(ev['range'])}</small></h2><pre><code>"
            + esc("\n".join(ev["lines"] or ["; no compact evidence lines matched"]))
            + "</code></pre></section>"
        )

    html_text = f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{esc(data['title'])}</title>
  <style>
    :root {{ color-scheme: light; --bg:#f5f7fb; --panel:#fff; --line:#d8dee9; --ink:#18202b; --muted:#5b6472; --good:#127a43; }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; font-family:Arial, "Malgun Gothic", sans-serif; background:var(--bg); color:var(--ink); }}
    main {{ max-width:1180px; margin:0 auto; padding:24px; }}
    h1 {{ margin:0 0 8px; font-size:26px; }}
    h2 {{ margin:0 0 12px; font-size:18px; }}
    small {{ color:var(--muted); font-weight:400; }}
    a {{ color:#1f5fbf; text-decoration:none; }}
    a:hover {{ text-decoration:underline; }}
    .nav {{ display:flex; gap:12px; flex-wrap:wrap; margin:0 0 18px; }}
    .card {{ background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:16px; margin:14px 0; box-shadow:0 1px 2px rgba(20,30,50,.05); }}
    .summary {{ margin:8px 0 0; padding-left:20px; }}
    .summary li {{ margin:6px 0; }}
    table {{ width:100%; border-collapse:collapse; font-size:13px; }}
    th, td {{ border:1px solid var(--line); padding:8px; vertical-align:top; }}
    th {{ background:#eef2f8; text-align:left; }}
    code, pre {{ font-family:Consolas, "Liberation Mono", monospace; }}
    code {{ background:#f0f3f8; padding:1px 4px; border-radius:4px; }}
    pre {{ overflow:auto; padding:12px; background:#111827; color:#d9e3f0; border-radius:6px; font-size:12px; line-height:1.45; }}
    pre code {{ background:transparent; padding:0; }}
    .tag {{ display:inline-block; border:1px solid #bfe3cc; border-radius:999px; padding:2px 7px; background:#eefaf2; color:var(--good); white-space:nowrap; }}
  </style>
</head>
<body>
<main>
  <h1>{esc(data['title'])}</h1>
  <div class="nav"><a href="../web/index.html">index</a><a href="battle_result_flag_lifecycle_review.html">result flag lifecycle</a><a href="battle_recovery_item_effect_review.html">recovery item effects</a><a href="battle_actor_display_wait_semantics_review.html">actor wait</a></div>
  <section class="card">
    <h2>결론</h2>
    <ul class="summary">{''.join(f'<li>{esc(item)}</li>' for item in data['summary'])}</ul>
  </section>
  <section class="card"><h2>Lifecycle</h2>{table(["stage", "VA", "operation", "meaning", "confidence"], lifecycle_rows)}</section>
  <section class="card"><h2>0x80 Skip Sites</h2>{table(["VA", "site", "test", "meaning"], skip_rows)}</section>
  <section class="card"><h2>Related Fields</h2>{table(["field", "meaning"], related_rows)}</section>
  {''.join(evidence_blocks)}
</main>
</body>
</html>
"""
    (OUT / "battle_knockout_inactive_flag_review.html").write_text(html_text, encoding="utf-8")


def main() -> None:
    OUT.mkdir(exist_ok=True)
    data = build_data()
    write_json(data)
    write_md(data)
    write_html(data)
    print("wrote battle_knockout_inactive_flag_review html/json/md")


if __name__ == "__main__":
    main()
