#!/usr/bin/env python3
"""Review target-result helper visuals for 0x20/0x08 battle result branches.

The target result script at 0x00454578 has three display paths:

* normal hit: helper operand 0x02, target frame 4
* 0x20 full miss / HP skip: helper operand 0x05, target frame 0
* 0x08 guard/glancing chip damage: helper operand 0x16, target frame 0

The previous branch report proved the wiring, but left the visual label open.
This pass follows those helper operands through the helper dispatch table and
child display VM scripts.  The helper operand is displayed in hex because the
script byte is written that way; the dispatch-table index is the same value as
an integer.
"""
from __future__ import annotations

import html
import json
import sys
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
TOOLS = ROOT / "tools"
OUT = ROOT / "out"
OUT_JSON = OUT / "battle_result_helper_label_review.json"
OUT_MD = OUT / "battle_result_helper_label_review.md"
OUT_HTML = OUT / "battle_result_helper_label_review.html"
RECT_SCAN_JSON = OUT / "cns_frame_rect_exe_scan.json"

if str(TOOLS) not in sys.path:
    sys.path.insert(0, str(TOOLS))

from build_battle_display_vm_static_decode import EXE, read_sections  # noqa: E402
from build_battle_helper_body_review import (  # noqa: E402
    CHILD_SCRIPT_TABLE_PTR_VA,
    HELPER_TABLE_VA,
    classify_body,
    disassemble,
    function_features,
    pe_sections,
    read_u32,
    va_to_offset,
)
from build_battle_helper_child_script_review import walk_child_script  # noqa: E402


TARGET_HELPERS = [
    {
        "case": "normal hit",
        "flag": "neither 0x20 nor 0x08",
        "scriptVaHex": "0x00454588",
        "helperOperandHex": "0x02",
        "wlkNo": None,
        "targetFrame": 4,
        "meaning": "normal target hit display path",
    },
    {
        "case": "0x20 full miss / HP apply skip",
        "flag": "target +0x62 & 0x20",
        "scriptVaHex": "0x0045459c",
        "helperOperandHex": "0x05",
        "wlkNo": 13,
        "targetFrame": 0,
        "meaning": "full miss / no HP apply target result path",
    },
    {
        "case": "0x08 guard/glancing chip damage",
        "flag": "target +0x62 & 0x08",
        "scriptVaHex": "0x004545b4",
        "helperOperandHex": "0x16",
        "wlkNo": 12,
        "targetFrame": 0,
        "meaning": "guard/glancing chip-damage target result path",
    },
]

SPRITE_ASSET = {
    "0x10": "btl_etc",
    "0x11": "btl_at",
    "0x14": "btl_rs",
    "0x17": "btl_sm",
    "0x1a": "btl_efc",
}
SPECIAL_LABEL_SELECTORS = {
    (0x10, 81): "MISS",
    (0x10, 82): "HIT",
}


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


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


def helper_int(hex_value: str) -> int:
    return int(hex_value, 16)


def next_function_stop(blob: bytes, sections: list[dict[str, int | str]], ptr: int) -> int:
    ptrs: list[int] = []
    for helper_id in range(0x80):
        entry_va = HELPER_TABLE_VA + helper_id * 4
        if va_to_offset(entry_va, sections) is None:
            break
        value = read_u32(blob, sections, entry_va)
        if value:
            ptrs.append(value)
    following = [value for value in sorted(set(ptrs)) if value > ptr]
    return min(following[0] if following else ptr + 0x500, ptr + 0x500)


def summarize_decoded(decoded: dict[str, Any]) -> dict[str, Any]:
    return {
        "startVaHex": decoded.get("startVaHex"),
        "stopReason": decoded.get("stopReason"),
        "instructionCount": decoded.get("instructionCount"),
        "opcodeCounts": decoded.get("opcodeCounts") or {},
        "frameSequence": decoded.get("frameSequence") or [],
        "initFrameSequence": decoded.get("initFrameSequence") or [],
        "frameScriptTargets": decoded.get("frameScriptTargets") or [],
        "randomRanges": decoded.get("randomRanges") or [],
        "waits": decoded.get("waits") or [],
        "parentActorLinks": decoded.get("parentActorLinks") or [],
        "rows": decoded.get("rows") or [],
    }


def child_head(blob: bytes, sections: list[dict[str, int | str]], va: int | None) -> str:
    if not va:
        return ""
    offset = va_to_offset(va, sections)
    if offset is None:
        return ""
    return blob[offset : offset + 48].hex(" ")


def load_rect_tables() -> dict[str, list[dict[str, Any]]]:
    if not RECT_SCAN_JSON.exists():
        return {}
    data = json.loads(RECT_SCAN_JSON.read_text(encoding="utf-8"))
    tables: dict[str, list[dict[str, Any]]] = {}
    for row in data.get("rows") or []:
        asset = row.get("asset")
        rects = ((row.get("bestTable") or {}).get("rects") or [])
        if asset and rects:
            tables[str(asset)] = rects
    return tables


def visual_frames(decoded: dict[str, Any], rect_tables: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]:
    visuals: list[dict[str, Any]] = []
    for item in decoded.get("rows") or []:
        if item.get("category") != "frame":
            continue
        sprite_hex = item.get("spriteHex")
        frame = item.get("frame")
        asset = SPRITE_ASSET.get(str(sprite_hex).lower(), "")
        rects = rect_tables.get(asset, [])
        rect = rects[frame] if isinstance(frame, int) and 0 <= frame < len(rects) else None
        visuals.append(
            {
                "sourceVaHex": item.get("vaHex"),
                "spriteHex": sprite_hex,
                "asset": asset,
                "frame": frame,
                "gate": item.get("gate"),
                "rect": rect,
            }
        )
    return visuals


def body_direct_visuals(
    blob: bytes,
    sections: list[dict[str, int | str]],
    function_va: int,
    stop_va: int,
    rect_tables: dict[str, list[dict[str, Any]]],
) -> list[dict[str, Any]]:
    start_offset = va_to_offset(function_va, sections)
    stop_offset = va_to_offset(stop_va, sections)
    if start_offset is None or stop_offset is None or stop_offset <= start_offset:
        return []
    raw = blob[start_offset:stop_offset]
    visuals: list[dict[str, Any]] = []
    for pos in range(0, max(0, len(raw) - 3)):
        value = int.from_bytes(raw[pos : pos + 4], "little")
        sprite = (value >> 16) & 0xFFFF
        frame = value & 0xFFFF
        label = SPECIAL_LABEL_SELECTORS.get((sprite, frame))
        if not label:
            continue
        sprite_hex = f"0x{sprite:02x}"
        asset = SPRITE_ASSET.get(sprite_hex, "")
        rects = rect_tables.get(asset, [])
        rect = rects[frame] if 0 <= frame < len(rects) else None
        opcode_va = function_va + pos - 1 if pos > 0 and raw[pos - 1] == 0x68 else None
        visuals.append(
            {
                "label": label,
                "selectorHex": f"0x{value:08x}",
                "sourceVaHex": hex32(function_va + pos),
                "pushOpcodeVaHex": hex32(opcode_va) if opcode_va is not None else "",
                "spriteHex": sprite_hex,
                "asset": asset,
                "frame": frame,
                "rect": rect,
                "evidence": "function-body immediate selector",
            }
        )
    return visuals


def build() -> dict[str, Any]:
    blob = EXE.read_bytes()
    rect_tables = load_rect_tables()
    _image_base, body_sections = pe_sections(blob)
    child_sections = read_sections(blob)
    child_table_base = read_u32(blob, body_sections, CHILD_SCRIPT_TABLE_PTR_VA)

    rows: list[dict[str, Any]] = []
    for target in TARGET_HELPERS:
        operand_hex = target["helperOperandHex"]
        helper_id = helper_int(operand_hex)
        function_va = read_u32(blob, body_sections, HELPER_TABLE_VA + helper_id * 4)
        stop_va = next_function_stop(blob, body_sections, function_va)
        disasm = disassemble(function_va, stop_va)
        features = function_features(disasm)
        direct_visuals = body_direct_visuals(blob, body_sections, function_va, stop_va, rect_tables)
        child_va = read_u32(blob, body_sections, child_table_base + helper_id * 4)
        child_decoded = walk_child_script(blob, child_sections, child_va, max_steps=180) if child_va else {}
        frame_script_decodes = []
        for frame_target in child_decoded.get("frameScriptTargets") or []:
            target_hex = frame_target.get("targetVaHex")
            if not target_hex:
                continue
            frame_decoded = walk_child_script(blob, child_sections, int(target_hex, 16), max_steps=180)
            summarized = summarize_decoded(frame_decoded)
            frame_script_decodes.append(
                {
                    "sourceVaHex": frame_target.get("vaHex"),
                    "targetVaHex": target_hex,
                    "decoded": summarized,
                    "visualFrames": visual_frames(summarized, rect_tables),
                }
            )

        visual_conclusion = "label not proven"
        if helper_id == 5:
            if direct_visuals:
                visual_conclusion = "helper body directly renders btl_etc MISS selector 0x00100051; no child frameScript is needed"
            else:
                visual_conclusion = "motion/helper only in decoded child VM; no direct MISS/HIT frameScript found"
        elif helper_id == 22:
            visual_conclusion = "child VM sets frameScript 0x004b4bf8 with sprite 0x1a frames 56..59 gate 2, bound to btl_efc rects"
        elif helper_id == 2:
            visual_conclusion = "normal hit helper produces target-relative motion but no separate label frameScript"

        rows.append(
            {
                **target,
                "helperIdDecimal": helper_id,
                "functionVaHex": hex32(function_va),
                "functionClass": classify_body(function_va, features),
                "featureSummary": {
                    "allocationCount": features.get("allocationCount"),
                    "targetRangeLoop": features.get("targetRangeLoop"),
                    "usesHelperIdAsScriptTableIndex": features.get("usesHelperIdAsScriptTableIndex"),
                    "scriptTableOffsets": features.get("scriptTableOffsets"),
                    "knownCalls": features.get("knownCalls"),
                    "writesDisplayXy": features.get("writesDisplayXy"),
                    "writesVisualIndex0x28": features.get("writesVisualIndex0x28"),
                },
                "childScriptVaHex": hex32(child_va) if child_va else "",
                "childScriptHeadBytes": child_head(blob, body_sections, child_va),
                "childDecoded": summarize_decoded(child_decoded),
                "frameScriptDecodes": frame_script_decodes,
                "directVisualFrames": direct_visuals,
                "visualConclusion": visual_conclusion,
                "disasmExcerpt": "\n".join(disasm.splitlines()[:28]),
            }
        )

    conclusions = [
        "Helper operands are script bytes. `0x16` is dispatch-table index decimal 22, so browser code should store both the hex operand and the decimal index.",
        "The 0x20 branch helper operand 0x05 is grounded. Its child VM contains motion/wait operations, while the helper function body directly pushes selector 0x00100051 and calls the render primitive, binding it to btl_etc frame 81 MISS.",
            "The 0x08 branch helper operand 0x16 is grounded and its child VM points at frameScript 0x004b4bf8, which emits sprite 0x1a frames 56,57,58,59 with gate 2. User observation matches this as a no-label guard/glancing chip-damage presentation over the normal target frame.",
        "Sprite 0x1a is bound to btl_efc. Frames 56..59 resolve to four 32x48 rects at y=16, so this is a small effect-sheet animation, not a btl_etc MISS/HIT label.",
    ]
    return {
        "version": 1,
        "kind": "battle-result-helper-label-review",
        "source": [
            "Hwanse2.exe",
            "out/battle_result_display_branch_review.json",
            "helper dispatch table 0x00454c10",
            "child script table pointer 0x00442da1",
        ],
        "status": "result-helper-special-branch-visuals-grounded",
        "summary": {
            "reviewedHelpers": len(rows),
            "helperOperands": [row["helperOperandHex"] for row in rows],
            "helperDecimalIds": [row["helperIdDecimal"] for row in rows],
            "promotedFrameScripts": sum(1 for row in rows if row["frameScriptDecodes"]),
        },
        "conclusions": conclusions,
        "rows": rows,
    }


def compact(values: Any) -> str:
    if not values:
        return "-"
    if isinstance(values, dict):
        return ", ".join(f"{key}:{value}" for key, value in values.items())
    return ", ".join(str(value) for value in values)


def visual_frame_text(frame: dict[str, Any]) -> str:
    rect = frame.get("rect") or {}
    if rect:
        rect_text = f"{rect.get('x')},{rect.get('y')} {rect.get('w')}x{rect.get('h')}"
    else:
        rect_text = "no rect"
    gate = frame.get("gate")
    gate_text = f"@{gate}" if gate is not None else ""
    label = f" {frame.get('label')}" if frame.get("label") else ""
    selector = f" {frame.get('selectorHex')}" if frame.get("selectorHex") else ""
    return f"{frame.get('asset') or '-'} F{frame.get('frame')}{gate_text}{label}{selector} ({rect_text})"


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Result Helper Label Review",
        "",
        f"- status: `{report['status']}`",
        f"- helper operands: `{compact(report['summary']['helperOperands'])}`",
        "",
        "## Conclusions",
        "",
    ]
    lines.extend(f"- {item}" for item in report["conclusions"])
    lines.extend(
        [
            "",
            "## Rows",
            "",
            "| case | flag | helper operand | decimal id | function | child script | visual conclusion | frame scripts |",
            "| --- | --- | --- | ---: | --- | --- | --- | --- |",
        ]
    )
    for row in report["rows"]:
        frame_text = " / ".join(
            f"{item['targetVaHex']} [{compact(item['decoded'].get('frameSequence'))}]"
            for item in row["frameScriptDecodes"]
        ) or "-"
        visual_text = " / ".join(
            visual_frame_text(frame)
            for item in row["frameScriptDecodes"]
            for frame in item.get("visualFrames") or []
        )
        direct_text = " / ".join(visual_frame_text(frame) for frame in row.get("directVisualFrames") or [])
        visual_text = " / ".join(part for part in [visual_text, direct_text] if part) or "-"
        if visual_text == "-":
            frame_cell = frame_text
        elif frame_text == "-":
            frame_cell = visual_text
        else:
            frame_cell = f"{frame_text}<br>{visual_text}"
        lines.append(
            f"| {row['case']} | `{row['flag']}` | `{row['helperOperandHex']}` | {row['helperIdDecimal']} | "
            f"`{row['functionVaHex']}` `{row['functionClass']}` | `{row['childScriptVaHex']}` | "
            f"{row['visualConclusion']} | {frame_cell} |"
        )
    return "\n".join(lines) + "\n"


def html_page(report: dict[str, Any]) -> str:
    notes = "".join(f"<li>{esc(item)}</li>" for item in report["conclusions"])
    rows = []
    preview_index = 0
    for row in report["rows"]:
        frame_parts = []
        direct_parts = []
        for frame in row.get("directVisualFrames") or []:
            rect = frame.get("rect") or {}
            if rect and frame.get("asset"):
                direct_parts.append(
                    "<figure class='sprite-card'>"
                    f"<canvas class='sprite-preview' data-asset='{esc(frame['asset'])}' "
                    f"data-x='{esc(rect.get('x'))}' data-y='{esc(rect.get('y'))}' "
                    f"data-w='{esc(rect.get('w'))}' data-h='{esc(rect.get('h'))}' data-scale='3'></canvas>"
                    f"<figcaption>{esc(visual_frame_text(frame))}<br>{esc(frame.get('pushOpcodeVaHex') or frame.get('sourceVaHex'))}</figcaption>"
                    "</figure>"
                )
            else:
                direct_parts.append(f"<span class='muted'>{esc(visual_frame_text(frame))}</span>")
        if direct_parts:
            frame_parts.append(
                "<div class='frame-script'>"
                "<strong>function-body direct render</strong>"
                f"<div class='sprite-row'>{''.join(direct_parts)}</div>"
                "</div>"
            )
        for item in row["frameScriptDecodes"]:
            visuals = []
            for frame in item.get("visualFrames") or []:
                rect = frame.get("rect") or {}
                if rect and frame.get("asset"):
                    preview_index += 1
                    visuals.append(
                        "<figure class='sprite-card'>"
                        f"<canvas class='sprite-preview' data-asset='{esc(frame['asset'])}' "
                        f"data-x='{esc(rect.get('x'))}' data-y='{esc(rect.get('y'))}' "
                        f"data-w='{esc(rect.get('w'))}' data-h='{esc(rect.get('h'))}' data-scale='3'></canvas>"
                        f"<figcaption>{esc(visual_frame_text(frame))}</figcaption>"
                        "</figure>"
                    )
                else:
                    visuals.append(f"<span class='muted'>{esc(visual_frame_text(frame))}</span>")
            frame_parts.append(
                "<div class='frame-script'>"
                f"<code>{esc(item['targetVaHex'])}</code> frames {esc(compact(item['decoded'].get('frameSequence')))}"
                f"<div class='sprite-row'>{''.join(visuals) if visuals else '<span class=\"muted\">no resolved sprite frames</span>'}</div>"
                "</div>"
            )
        frame_text = "".join(frame_parts) or "-"
        feature = row["featureSummary"]
        rows.append(
            "<tr>"
            f"<td>{esc(row['case'])}</td>"
            f"<td><code>{esc(row['flag'])}</code></td>"
            f"<td><code>{esc(row['helperOperandHex'])}</code><br>dec {esc(row['helperIdDecimal'])}</td>"
            f"<td><code>{esc(row['wlkNo'] if row['wlkNo'] is not None else '-')}</code></td>"
            f"<td><code>{esc(row['targetFrame'])}</code></td>"
            f"<td><code>{esc(row['functionVaHex'])}</code><br>{esc(row['functionClass'])}</td>"
            f"<td><code>{esc(row['childScriptVaHex'])}</code><br><span class='muted'>{esc(row['childScriptHeadBytes'])}</span></td>"
            f"<td>{esc(row['visualConclusion'])}</td>"
            f"<td>{frame_text}</td>"
            f"<td>alloc={esc(feature.get('allocationCount'))}<br>id-index={esc(feature.get('usesHelperIdAsScriptTableIndex'))}<br>"
            f"calls={esc(compact(feature.get('knownCalls')))}</td>"
            f"<td><details><summary>child rows</summary><pre>{esc(json.dumps(row['childDecoded']['rows'], ensure_ascii=False, indent=2))}</pre></details>"
            f"<details><summary>disasm</summary><pre>{esc(row['disasmExcerpt'])}</pre></details></td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>Battle Result Helper Label Review</title>
  <style>
    body {{ margin: 0; font: 13px system-ui, sans-serif; background: #101318; color: #e9edf3; }}
    main {{ padding: 18px; }}
    a {{ color: #8cc8ff; }}
    table {{ width: 100%; border-collapse: collapse; margin-top: 14px; }}
    th, td {{ border: 1px solid #29303a; padding: 8px; vertical-align: top; }}
    th {{ background: #171c24; color: #aeb8c5; position: sticky; top: 0; }}
    code, pre {{ background: #0b0e13; color: #d5e7ff; }}
    pre {{ white-space: pre-wrap; max-height: 360px; overflow: auto; padding: 8px; }}
    .muted {{ color: #9ca6b3; }}
    .nav {{ display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }}
    .frame-script {{ display: grid; gap: 8px; min-width: 240px; }}
    .sprite-row {{ display: flex; gap: 8px; flex-wrap: wrap; align-items: start; margin-top: 6px; }}
    .sprite-card {{ margin: 0; display: grid; gap: 4px; justify-items: center; }}
    .sprite-card figcaption {{ max-width: 120px; color: #aeb8c5; font-size: 11px; text-align: center; }}
    .sprite-preview {{ image-rendering: pixelated; background: #202630; border: 1px solid #56606d; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a href="../web/index.html">index</a>
    <a href="../web/battle_simulator.html">battle skill runner</a>
    <a href="battle_result_display_branch_review.html">result display branches</a>
  </div>
  <h1>Battle Result Helper Label Review</h1>
  <p>status: <code>{esc(report['status'])}</code></p>
  <ul>{notes}</ul>
  <table>
    <thead><tr><th>case</th><th>flag</th><th>helper</th><th>WLK</th><th>target frame</th><th>function</th><th>child script</th><th>visual conclusion</th><th>frame scripts</th><th>features</th><th>details</th></tr></thead>
    <tbody>{''.join(rows)}</tbody>
  </table>
</main>
<script>
  const imageCache = new Map();
  function loadImage(src) {{
    if (imageCache.has(src)) return imageCache.get(src);
    const promise = new Promise((resolve, reject) => {{
      const image = new Image();
      image.onload = () => resolve(image);
      image.onerror = () => reject(new Error(`image load failed: ${{src}}`));
      image.src = src;
    }});
    imageCache.set(src, promise);
    return promise;
  }}
  async function drawPreviews() {{
    const canvases = [...document.querySelectorAll("canvas[data-asset]")];
    for (const canvas of canvases) {{
      const asset = canvas.dataset.asset;
      const image = await loadImage(`${{asset}}.png`);
      const sx = Number(canvas.dataset.x), sy = Number(canvas.dataset.y);
      const sw = Number(canvas.dataset.w), sh = Number(canvas.dataset.h);
      const scale = Number(canvas.dataset.scale) || 3;
      canvas.width = sw * scale;
      canvas.height = sh * scale;
      const ctx = canvas.getContext("2d");
      ctx.imageSmoothingEnabled = false;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(image, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
    }}
  }}
  drawPreviews().catch((error) => console.warn(error));
</script>
</body>
</html>
"""


def main() -> None:
    report = build()
    OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    OUT_MD.write_text(markdown(report), encoding="utf-8")
    OUT_HTML.write_text(html_page(report), encoding="utf-8")
    print(f"wrote {OUT_JSON}")


if __name__ == "__main__":
    main()
