#!/usr/bin/env python3
"""Build a focused review for actor +0x62 bit 0x01.

The result flag report documents all known actor +0x62 bits.  This report
narrows one specific bit that became important while tracing revive items:
bit 0x01 is the short-lived actor latch used after one-turn statuses and after
revival resets.  It is also part of the action-script gate mask 0x83.
"""
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


PRODUCER_ROWS = [
    {
        "va": 0x00435049,
        "source": "status result 1",
        "writes": "+0x2a=0x17, +0x64=0, +0x62|=0x01, +0x68=1",
        "meaning": "상태 result 1: 넘어짐. 1턴 행동불가 latch를 세운다.",
        "confidence": "확정: status mapping + bit write",
    },
    {
        "va": 0x00435074,
        "source": "status result 2",
        "writes": "+0x2a=0x18, +0x64=0, +0x62|=0x01, +0x68=1",
        "meaning": "상태 result 2: 휙 날아감. 1턴 행동불가 latch를 세운다.",
        "confidence": "확정: status mapping + bit write",
    },
    {
        "va": 0x0043509F,
        "source": "status result 3",
        "writes": "+0x2a=0x16, +0x64=0, +0x62|=0x01, +0x68=1",
        "meaning": "상태 result 3: 행동정지. 1턴 행동불가 latch를 세운다.",
        "confidence": "확정: status mapping + bit write",
    },
    {
        "va": 0x00434996,
        "source": "paralysis cure / antidote path",
        "writes": "+0x62|=0x01, +0x2a=+0x66, +0x66=0, +0x64=0, +0x62&=0xfd",
        "meaning": "마비 해제 시 timed lock 0x02를 지우고 1턴 표시/게이트 latch를 세운다.",
        "confidence": "확정",
    },
    {
        "va": 0x00435880,
        "source": "refresh water revive prelude",
        "writes": "if +0x62&0x80: restore +0x2a from +0x66, clear +0x66/+0x64, +0x62=0x01",
        "meaning": "기절/비활성 actor를 active actor로 되돌리면서 0x40/0x80을 덮어 지운다.",
        "confidence": "확정",
    },
    {
        "va": 0x00435951,
        "source": "beast stone revive prelude",
        "writes": "if +0x62&0x80: restore +0x2a from +0x66, clear +0x66/+0x64, +0x62=0x01",
        "meaning": "마수석도 리프레시 워터와 같은 revive latch를 사용한다.",
        "confidence": "확정",
    },
]


CONSUMER_ROWS = [
    {
        "va": 0x0040DB36,
        "consumer": "action-script current-order gate",
        "test": "test actor+0x62, 0x83",
        "if_set": "context+0x40 = [stream+4]",
        "if_clear": "context+0x40 += 8",
        "meaning": "0x01, 0x02, 0x80 actor는 정상 action script 흐름 대신 branch target으로 간다.",
    },
    {
        "va": 0x0040D95B,
        "consumer": "battle-order one-turn cleanup",
        "test": "skip 0x80 actors, then test actor+0x62 bit 0x01",
        "if_set": "restore +0x2a from +0x66 when present; clear +0x66/+0x64; actor+0x62 &= 0xfe",
        "if_clear": "no one-turn cleanup",
        "meaning": "0x01은 다음 battle-order pass에서 정리되는 1턴 latch다.",
    },
]


RELATED_FIELDS = [
    ("actor+0x2a", "현재 자세/상태 id. 0x16/0x17/0x18 같은 상태 id가 들어간다."),
    ("actor+0x62", "전투 actor flag byte. 이 문서는 bit 0x01만 추적한다."),
    ("actor+0x64", "상태 timer/counter. 0x01 생산자들은 대체로 0으로 비운다."),
    ("actor+0x66", "prior mode/status save slot. cleanup/revive/cure에서 +0x2a 복원에 사용."),
    ("actor+0x68", "display/status dirty marker. 상태 후보 생산자가 1로 세운다."),
    ("0x59dd60", "battle-order actor count."),
    ("0x59e2b0", "battle-order index list."),
    ("0x59db30", "actor pointer table."),
    ("0x59e300", "current battle-order cursor used by the action gate."),
]


EVIDENCE_SPECS = [
    {
        "label": "battle-order cleanup consumes +0x62 bit 0x01",
        "range": (0x0040D95B, 0x0040DA05),
        "needles": ["[eax+0x62]", "test   cl,0x1", "[eax+0x66]", "[ecx+0x2a]", "[eax+0x64]", "and    cl,0xfe"],
        "context": 2,
    },
    {
        "label": "action script gate masks 0x01/0x02/0x80 together",
        "range": (0x0040DB36, 0x0040DB8C),
        "needles": ["0x59e300", "0x59e2b0", "0x59db30", "[eax+0x62]", "test   cl,0x83", "[eax+0x4]", "add    DWORD PTR [eax+0x40],0x8"],
        "context": 2,
    },
    {
        "label": "status results 1..3 produce +0x62 bit 0x01",
        "range": (0x00435039, 0x004350C8),
        "needles": ["[eax+0x2a],0x17", "[eax+0x2a],0x18", "[eax+0x2a],0x16", "or     cl,0x1", "[eax+0x68],0x1"],
        "context": 2,
    },
    {
        "label": "antidote/paralysis cure sets 0x01 and clears 0x02",
        "range": (0x0043497D, 0x00434A45),
        "needles": ["[eax+0x2a]", "or     cl,0x1", "[eax+0x66]", "[eax+0x64]", "and    cl,0xfd"],
        "context": 2,
    },
    {
        "label": "revive consumables overwrite +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 One-Turn Actor Flag Review",
        "subject": "actor +0x62 bit 0x01",
        "summary": [
            "actor +0x62 bit 0x01 is a short-lived actor latch, not a damage/critical bit.",
            "It is produced by one-turn status results, paralysis cure, and revive consumables.",
            "The action-script gate at 0x40db36 tests mask 0x83, so 0x01 shares skip/branch behavior with timed lock 0x02 and inactive/display-skip 0x80.",
            "The battle-order cleanup at 0x40d95b clears 0x01 after restoring +0x2a from +0x66 and clearing +0x64/+0x66.",
            "Revive consumables intentionally overwrite +0x62 with 0x01, clearing previous 0x40/0x80 knockout/inactive flags while leaving the actor in a one-turn display/action-gated state.",
        ],
        "producerRows": PRODUCER_ROWS,
        "consumerRows": CONSUMER_ROWS,
        "relatedFields": RELATED_FIELDS,
        "evidence": evidence,
    }


def write_json(data: dict[str, Any]) -> None:
    (OUT / "battle_one_turn_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 One-Turn Actor Flag Review",
        "",
        "`actor +0x62 bit 0x01`은 데미지/크리티컬 플래그가 아니라 전투 actor의 1턴 latch다.",
        "",
        "## 결론",
        "",
    ]
    for item in data["summary"]:
        lines.append(f"- {item}")
    lines.extend(["", "## Producers", ""])
    lines.append("| VA | Source | Writes | Meaning | Confidence |")
    lines.append("|---|---|---|---|---|")
    for row in data["producerRows"]:
        lines.append(
            f"| `{hex32(row['va'])}` | {row['source']} | `{row['writes']}` | {row['meaning']} | {row['confidence']} |"
        )
    lines.extend(["", "## Consumers", ""])
    lines.append("| VA | Consumer | Test | If set | If clear | Meaning |")
    lines.append("|---|---|---|---|---|---|")
    for row in data["consumerRows"]:
        lines.append(
            f"| `{hex32(row['va'])}` | {row['consumer']} | `{row['test']}` | `{row['if_set']}` | `{row['if_clear']}` | {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_one_turn_flag_review.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")


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


def write_html(data: dict[str, Any]) -> None:
    producer_rows = [
        [
            f"<code>{hex32(row['va'])}</code>",
            esc(row["source"]),
            f"<code>{esc(row['writes'])}</code>",
            esc(row["meaning"]),
            f"<span class=\"tag good\">{esc(row['confidence'])}</span>",
        ]
        for row in data["producerRows"]
    ]
    consumer_rows = [
        [
            f"<code>{hex32(row['va'])}</code>",
            esc(row["consumer"]),
            f"<code>{esc(row['test'])}</code>",
            f"<code>{esc(row['if_set'])}</code>",
            f"<code>{esc(row['if_clear'])}</code>",
            esc(row["meaning"]),
        ]
        for row in data["consumerRows"]
    ]
    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; --warn:#946200; }}
    * {{ 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 var(--line); border-radius:999px; padding:2px 7px; background:#f6f8fb; white-space:nowrap; }}
    .tag.good {{ color:var(--good); border-color:#bfe3cc; background:#eefaf2; }}
  </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></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>Producers</h2>
    {table(["VA", "source", "writes", "meaning", "confidence"], producer_rows)}
  </section>
  <section class="card">
    <h2>Consumers</h2>
    {table(["VA", "consumer", "test", "if set", "if clear", "meaning"], consumer_rows)}
  </section>
  <section class="card">
    <h2>Related Fields</h2>
    {table(["field", "meaning"], related_rows)}
  </section>
  {''.join(evidence_blocks)}
</main>
</body>
</html>
"""
    (OUT / "battle_one_turn_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_one_turn_flag_review html/json/md")


if __name__ == "__main__":
    main()
