#!/usr/bin/env python3
"""Ground the battle damage number display path.

The battle result layer already proves where damage is computed and applied.
This report focuses only on the visual number layer:

* target actor +0x6e result value is read by battle helper bodies
* the value is passed to 0x42119b
* 0x42119b dynamically builds btl_etc digit selectors from frame base 0x36
* selector 0x00100036..0x0010003f maps to btl_etc frames 54..63

This keeps the browser runner's damage-number drawing rule separate from the
formula itself.
"""
from __future__ import annotations

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


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

IMAGE_BASE = 0x00400000
DIGIT_RENDER_FUNCTION_VA = 0x0042119B
DIGIT_RENDER_FUNCTION_END_VA = 0x004213B6
RENDER_PRIMITIVE_VA = 0x004175D3
SELECTOR_BASE_ADD_VA = 0x0042129F
SELECTOR_SPRITE_BASE_ADD_VA = 0x004212A2
LOOP_RENDER_CALL_VA = 0x004213A9


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


def hex32(value: int | None) -> str:
    return "" if value is None else f"0x{value:08x}"


def pe_sections(blob: bytes) -> tuple[int, list[dict[str, int | str]]]:
    pe_offset = struct.unpack_from("<I", blob, 0x3C)[0]
    opt_size = struct.unpack_from("<H", blob, pe_offset + 20)[0]
    image_base = struct.unpack_from("<I", blob, pe_offset + 52)[0]
    section_count = struct.unpack_from("<H", blob, pe_offset + 6)[0]
    section_base = pe_offset + 24 + opt_size
    sections: list[dict[str, int | str]] = []
    for index in range(section_count):
        offset = section_base + index * 40
        name = blob[offset : offset + 8].rstrip(b"\0").decode("ascii", "replace")
        virtual_size, virtual_address, raw_size, raw_ptr = struct.unpack_from("<IIII", blob, offset + 8)
        sections.append(
            {
                "name": name,
                "virtualAddress": virtual_address,
                "virtualSize": virtual_size,
                "rawSize": raw_size,
                "rawPtr": raw_ptr,
            }
        )
    return image_base, sections


def va_to_offset(va: int, sections: list[dict[str, int | str]], image_base: int = IMAGE_BASE) -> int | None:
    rva = va - image_base
    for section in sections:
        start = int(section["virtualAddress"])
        span = max(int(section["virtualSize"]), int(section["rawSize"]))
        if start <= rva < start + span:
            return int(section["rawPtr"]) + (rva - start)
    return None


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 scan_pattern(blob: bytes, sections: list[dict[str, int | str]], pattern: bytes) -> list[int]:
    text = next(section for section in sections if section["name"] == ".text")
    raw_ptr = int(text["rawPtr"])
    raw_size = int(text["rawSize"])
    text_blob = blob[raw_ptr : raw_ptr + raw_size]
    text_va = IMAGE_BASE + int(text["virtualAddress"])
    hits: list[int] = []
    start = 0
    while True:
        pos = text_blob.find(pattern, start)
        if pos < 0:
            return hits
        hits.append(text_va + pos)
        start = pos + 1


def load_btl_etc_rects() -> tuple[dict[str, Any], list[dict[str, Any]]]:
    data = json.loads(RECT_SCAN_JSON.read_text(encoding="utf-8"))
    row = next(item for item in data.get("rows") or [] if item.get("asset") == "btl_etc")
    table = row.get("bestTable") or {}
    return table, table.get("rects") or []


def digit_rect_rows(rects: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for digit in range(10):
        frame = 54 + digit
        selector = 0x00100036 + digit
        rect = rects[frame] if frame < len(rects) else {}
        rows.append(
            {
                "digit": digit,
                "frame": frame,
                "selectorHex": f"0x{selector:08x}",
                "rect": rect,
                "rectText": f"{rect.get('x')},{rect.get('y')} {rect.get('w')}x{rect.get('h')}" if rect else "",
            }
        )
    return rows


def call_site_rows() -> list[dict[str, Any]]:
    specs = [
        {
            "callVa": 0x004118F0,
            "functionVa": 0x004117F0,
            "label": "target/helper damage value display",
            "modeArg": 8,
            "widthArg": 4,
            "valueSource": "target actor +0x6e",
            "evidence": "mov cx,[eax+0x6e]; push ecx; push 4; push 8; push object; call 0x42119b",
        },
        {
            "callVa": 0x0041268F,
            "functionVa": 0x0041254A,
            "label": "battle target result damage popup",
            "modeArg": 8,
            "widthArg": 4,
            "valueSource": "target actor +0x6e",
            "evidence": "mov cx,[target+0x6e]; push ecx; push 4; push 8; call 0x42119b",
        },
        {
            "callVa": 0x00412995,
            "functionVa": 0x00412818,
            "label": "multi-target/all-target result damage popup",
            "modeArg": 10,
            "widthArg": 4,
            "valueSource": "current target actor +0x6e",
            "evidence": "loop target; mov cx,[actor+0x6e]; push ecx; push 4; push 0x0a; call 0x42119b",
        },
        {
            "callVa": 0x00411B1B,
            "functionVa": 0x00411A1C,
            "label": "auxiliary value display",
            "modeArg": 9,
            "widthArg": 4,
            "valueSource": "target actor +0x44",
            "evidence": "mov cx,[actor+0x44]; push ecx; push 4; push 9; call 0x42119b",
        },
    ]
    rows = []
    for spec in specs:
        excerpt = disassemble(spec["callVa"] - 0x18, spec["callVa"] + 0x10)
        rows.append({**spec, "callVaHex": hex32(spec["callVa"]), "functionVaHex": hex32(spec["functionVa"]), "excerpt": excerpt})
    return rows


def selector_scan_rows(blob: bytes, sections: list[dict[str, int | str]]) -> list[dict[str, Any]]:
    patterns = [
        ("digit selector base add", bytes.fromhex("83 c0 36"), "add eax,0x36"),
        ("sprite selector base add", bytes.fromhex("05 00 00 10 00"), "add eax,0x100000"),
        ("literal digit0 selector", bytes.fromhex("36 00 10 00"), "0x00100036"),
        ("literal digit9 selector", bytes.fromhex("3f 00 10 00"), "0x0010003f"),
        ("literal MISS selector", bytes.fromhex("51 00 10 00"), "0x00100051"),
    ]
    rows = []
    for label, pattern, meaning in patterns:
        hits = scan_pattern(blob, sections, pattern)
        rows.append(
            {
                "label": label,
                "meaning": meaning,
                "hitCount": len(hits),
                "hits": [hex32(hit) for hit in hits],
            }
        )
    return rows


def build() -> dict[str, Any]:
    blob = EXE.read_bytes()
    image_base, sections = pe_sections(blob)
    btl_etc_table, btl_etc_rects = load_btl_etc_rects()
    digit_rows = digit_rect_rows(btl_etc_rects)
    disasm = disassemble(DIGIT_RENDER_FUNCTION_VA, DIGIT_RENDER_FUNCTION_END_VA)
    important_lines = []
    for line in disasm.splitlines():
        if any(token in line for token in ["0x36", "0x100000", "0xa", "0x4175d3", "div", "[ebp+0x14]", "[ebp-0x18]"]):
            important_lines.append(line)
    data = {
        "version": 1,
        "kind": "battle-damage-digit-display-review",
        "source": str(Path(__file__).relative_to(ROOT)),
        "status": "damage-digit-display-grounded",
        "exe": {
            "path": str(EXE.relative_to(ROOT)),
            "imageBaseHex": hex32(image_base),
        },
        "summary": [
            "Damage values are rendered through function 0x0042119b.",
            "For damage-number mode, 0x0042129f adds frame base 0x36 and 0x004212a2 adds selector sprite base 0x00100000, producing selector base 0x00100036.",
            "The render loop divides the value by 10, uses the remainder as digit, and calls render primitive 0x004175d3 with selector base + digit.",
            "The selector range 0x00100036..0x0010003f maps to btl_etc frames 54..63, rectangles x=192..264 y=64, 8x16.",
            "Battle result callers read target actor +0x6e and pass it to 0x42119b, so +0x6e is both result value and visual damage-number input.",
        ],
        "function": {
            "vaHex": hex32(DIGIT_RENDER_FUNCTION_VA),
            "endVaHex": hex32(DIGIT_RENDER_FUNCTION_END_VA),
            "selectorBaseAddVaHex": hex32(SELECTOR_BASE_ADD_VA),
            "selectorSpriteBaseAddVaHex": hex32(SELECTOR_SPRITE_BASE_ADD_VA),
            "renderPrimitiveVaHex": hex32(RENDER_PRIMITIVE_VA),
            "loopRenderCallVaHex": hex32(LOOP_RENDER_CALL_VA),
            "importantLines": important_lines,
            "disassembly": disasm,
        },
        "btlEtcTable": {
            "tableStartVaHex": btl_etc_table.get("tableStartVaHex"),
            "frameCount": btl_etc_table.get("frameCount"),
            "imageSize": btl_etc_table.get("imageSize"),
            "boundaryKind": btl_etc_table.get("boundaryKind"),
        },
        "digitRows": digit_rows,
        "callSiteRows": call_site_rows(),
        "selectorScanRows": selector_scan_rows(blob, sections),
        "runnerRule": {
            "file": "web/battle_simulator.html",
            "function": "drawDamageDigits",
            "rule": "rects[54 + Number(char)]",
            "status": "matches EXE selector base 0x00100036 / btl_etc frame 54",
        },
    }
    return data


def write_json(data: dict[str, Any]) -> None:
    (OUT / "battle_damage_digit_display_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 Damage Digit Display Review",
        "",
        f"- status: `{data['status']}`",
        f"- function: `{data['function']['vaHex']}`",
        "",
        "## Conclusions",
        "",
    ]
    lines.extend(f"- {item}" for item in data["summary"])
    lines.extend(["", "## Digit Frames", ""])
    lines.append("| digit | selector | btl_etc frame | rect |")
    lines.append("|---:|---|---:|---|")
    for row in data["digitRows"]:
        lines.append(f"| {row['digit']} | `{row['selectorHex']}` | {row['frame']} | {row['rectText']} |")
    lines.extend(["", "## Call Sites", ""])
    lines.append("| call | label | args | value source | evidence |")
    lines.append("|---|---|---|---|---|")
    for row in data["callSiteRows"]:
        lines.append(
            f"| `{row['callVaHex']}` | {row['label']} | mode `{row['modeArg']}`, width `{row['widthArg']}` | "
            f"{row['valueSource']} | {row['evidence']} |"
        )
    lines.extend(["", "## Selector Scan", ""])
    lines.append("| pattern | meaning | hits |")
    lines.append("|---|---|---|")
    for row in data["selectorScanRows"]:
        lines.append(f"| {row['label']} | `{row['meaning']}` | {row['hitCount']} {' '.join(f'`{hit}`' for hit in row['hits'])} |")
    (OUT / "battle_damage_digit_display_review.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")


def digit_preview_html(rows: list[dict[str, Any]]) -> str:
    cards = []
    for row in rows:
        rect = row.get("rect") or {}
        style = (
            f"width:{int(rect.get('w') or 0) * 4}px;height:{int(rect.get('h') or 0) * 4}px;"
            f"background-image:url('btl_etc.png');"
            f"background-position:-{int(rect.get('x') or 0) * 4}px -{int(rect.get('y') or 0) * 4}px;"
            f"background-size:{384 * 4}px {96 * 4}px;"
        )
        cards.append(
            "<figure class='digit-card'>"
            f"<div class='sprite' style=\"{style}\"></div>"
            f"<figcaption>{row['digit']}<br><code>{row['selectorHex']}</code><br>F{row['frame']} {esc(row['rectText'])}</figcaption>"
            "</figure>"
        )
    return "".join(cards)


def write_html(data: dict[str, Any]) -> None:
    summary = "".join(f"<li>{esc(item)}</li>" for item in data["summary"])
    digit_rows = "".join(
        f"<tr><td>{row['digit']}</td><td><code>{esc(row['selectorHex'])}</code></td><td>{row['frame']}</td><td>{esc(row['rectText'])}</td></tr>"
        for row in data["digitRows"]
    )
    call_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['callVaHex'])}</code></td>"
        f"<td>{esc(row['label'])}</td>"
        f"<td>mode <code>{row['modeArg']}</code>, width <code>{row['widthArg']}</code></td>"
        f"<td>{esc(row['valueSource'])}</td>"
        f"<td>{esc(row['evidence'])}</td>"
        f"<td><pre>{esc(row['excerpt'])}</pre></td>"
        "</tr>"
        for row in data["callSiteRows"]
    )
    scan_rows = "".join(
        "<tr>"
        f"<td>{esc(row['label'])}</td>"
        f"<td><code>{esc(row['meaning'])}</code></td>"
        f"<td>{row['hitCount']}</td>"
        f"<td>{' '.join(f'<code>{esc(hit)}</code>' for hit in row['hits'])}</td>"
        "</tr>"
        for row in data["selectorScanRows"]
    )
    important_lines = "\n".join(data["function"]["importantLines"])
    html_text = f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Battle Damage Digit Display Review</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:1220px; margin:0 auto; padding:24px; }}
    h1 {{ margin:0 0 8px; font-size:26px; }}
    h2 {{ margin:0 0 12px; font-size:18px; }}
    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 {{ font-family:Consolas, "Liberation Mono", monospace; background:#f0f3f8; padding:1px 4px; border-radius:4px; }}
    pre {{ white-space:pre-wrap; max-height:260px; overflow:auto; margin:0; font-size:12px; }}
    .tag {{ display:inline-block; border:1px solid #bfe3cc; border-radius:999px; padding:2px 7px; background:#eefaf2; color:var(--good); white-space:nowrap; }}
    .digit-grid {{ display:flex; flex-wrap:wrap; gap:10px; }}
    .digit-card {{ margin:0; display:grid; justify-items:center; gap:6px; border:1px solid var(--line); background:#f8fafc; padding:8px; border-radius:6px; }}
    .digit-card figcaption {{ text-align:center; font-size:12px; color:var(--muted); }}
    .sprite {{ image-rendering:pixelated; background-color:transparent; background-repeat:no-repeat; }}
  </style>
</head>
<body>
<main>
  <h1>Battle Damage Digit Display Review</h1>
  <div class="nav">
    <a href="../web/index.html">index</a>
    <a href="../web/battle_simulator.html">battle skill runner</a>
    <a href="battle_result_layer_trace_review.html">result layer trace</a>
    <a href="battle_damage_formula_trace_review.html">damage formula trace</a>
    <a href="battle_result_display_branch_review.html">result display branches</a>
  </div>
  <section class="card">
    <p>status: <span class="tag">{esc(data['status'])}</span></p>
    <ul class="summary">{summary}</ul>
  </section>
  <section class="card">
    <h2>Digit Preview</h2>
    <div class="digit-grid">{digit_preview_html(data['digitRows'])}</div>
  </section>
  <section class="card">
    <h2>Digit Frames</h2>
    <table><thead><tr><th>digit</th><th>selector</th><th>btl_etc frame</th><th>rect</th></tr></thead><tbody>{digit_rows}</tbody></table>
  </section>
  <section class="card">
    <h2>0x42119b Key Lines</h2>
    <p>selector base add: <code>{esc(data['function']['selectorBaseAddVaHex'])}</code>, render loop call: <code>{esc(data['function']['loopRenderCallVaHex'])}</code></p>
    <pre>{esc(important_lines)}</pre>
  </section>
  <section class="card">
    <h2>Call Sites</h2>
    <table><thead><tr><th>call</th><th>label</th><th>args</th><th>value source</th><th>evidence</th><th>excerpt</th></tr></thead><tbody>{call_rows}</tbody></table>
  </section>
  <section class="card">
    <h2>Selector Scan</h2>
    <table><thead><tr><th>pattern</th><th>meaning</th><th>hits</th><th>locations</th></tr></thead><tbody>{scan_rows}</tbody></table>
  </section>
</main>
</body>
</html>
"""
    (OUT / "battle_damage_digit_display_review.html").write_text(html_text, encoding="utf-8")


def main() -> None:
    OUT.mkdir(exist_ok=True)
    data = build()
    write_json(data)
    write_md(data)
    write_html(data)
    print(f"wrote {OUT / 'battle_damage_digit_display_review.html'}")


if __name__ == "__main__":
    main()
