#!/usr/bin/env python3
"""Decode display frameScript streams referenced by battle helper child scripts.

The 0xbd helper body pass identifies helper functions, and the child-script pass
identifies spawned display objects.  Some of those child scripts attach another
VM stream through opcode 0x20 into display.frameScript(+0x64).  This report
decodes those frameScript targets so helper effects can be reviewed as their
own frame/gate/RNG streams instead of opaque pointers.
"""
from __future__ import annotations

import html
import json
from collections import Counter, defaultdict
from typing import Any

from build_battle_display_vm_static_decode import EXE, OUT, hex32, read_sections
from build_battle_helper_child_script_review import esc, vec, walk_child_script


HELPER_CHILD_JSON = OUT / "battle_helper_child_script_review.json"


def int_from_hex(value: str | None) -> int | None:
    if not value:
        return None
    try:
        return int(value, 16)
    except ValueError:
        return None


def compact_skills(skills: list[dict[str, Any]], limit: int = 8) -> str:
    values = [
        f"{skill.get('ownerName')} {skill.get('skillName')} {skill.get('skillIdHex')}"
        for skill in skills
    ]
    if len(values) > limit:
        values = values[:limit] + [f"... +{len(values) - limit}"]
    return "; ".join(values) or "-"


def frame_gate_sequence(decoded: dict[str, Any]) -> list[dict[str, Any]]:
    rows = []
    for frame in decoded.get("frames") or []:
        rows.append(
            {
                "vaHex": frame.get("vaHex"),
                "spriteHex": frame.get("spriteHex"),
                "frame": frame.get("frame"),
                "gate": frame.get("gate"),
                "label": f"{frame.get('frame')}@{frame.get('gate')}",
            }
        )
    return rows


def target_refs(report: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
    refs: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for helper in report.get("helperRows") or []:
        decoded = helper.get("decoded") or {}
        for target in decoded.get("frameScriptTargets") or []:
            target_hex = target.get("targetVaHex")
            if not target_hex:
                continue
            refs[target_hex].append(
                {
                    "helperId": helper.get("helperId"),
                    "childScriptVaHex": helper.get("childScriptVaHex"),
                    "sourceVaHex": target.get("vaHex"),
                    "functionVaHex": helper.get("functionVaHex"),
                    "bodyClass": helper.get("bodyClass"),
                    "skillRows": helper.get("skillRows") or [],
                    "openingObservations": helper.get("openingObservations") or [],
                }
            )
    return refs


def build() -> dict[str, Any]:
    data = EXE.read_bytes()
    sections = read_sections(data)
    child_report = json.loads(HELPER_CHILD_JSON.read_text(encoding="utf-8"))
    refs_by_target = target_refs(child_report)

    opcode_counter: Counter[str] = Counter()
    frame_script_rows = []
    helper_to_targets: defaultdict[int, list[str]] = defaultdict(list)
    skill_to_targets: defaultdict[str, set[str]] = defaultdict(set)

    for target_hex in sorted(refs_by_target, key=lambda item: int(item, 16)):
        target_va = int_from_hex(target_hex)
        decoded = walk_child_script(data, sections, target_va) if target_va is not None else {}
        opcode_counter.update(decoded.get("opcodeCounts") or {})
        frame_sequence = frame_gate_sequence(decoded)
        refs = refs_by_target[target_hex]
        helper_ids = sorted({ref.get("helperId") for ref in refs if ref.get("helperId") is not None})
        skill_rows = []
        seen_skills = set()
        for ref in refs:
            helper_id = ref.get("helperId")
            if helper_id is not None:
                helper_to_targets[int(helper_id)].append(target_hex)
            for skill in ref.get("skillRows") or []:
                key = (skill.get("ownerKey"), skill.get("skillIdHex"), skill.get("skillName"))
                if key in seen_skills:
                    continue
                seen_skills.add(key)
                skill_rows.append(skill)
                skill_to_targets[f"{skill.get('ownerName')}::{skill.get('skillName')}::{skill.get('skillIdHex')}"].add(target_hex)
        frame_script_rows.append(
            {
                "targetVa": target_va,
                "targetVaHex": target_hex,
                "helperIds": helper_ids,
                "helperRefs": refs,
                "skillRows": skill_rows,
                "stopReason": decoded.get("stopReason"),
                "instructionCount": decoded.get("instructionCount"),
                "opcodeCounts": decoded.get("opcodeCounts") or {},
                "frameGateSequence": frame_sequence,
                "frameSequence": [item.get("frame") for item in frame_sequence],
                "gateSequence": [item.get("gate") for item in frame_sequence],
                "spriteHexSequence": [item.get("spriteHex") for item in frame_sequence],
                "initFrameSequence": decoded.get("initFrameSequence") or [],
                "randomRanges": decoded.get("randomRanges") or [],
                "nestedFrameScriptTargets": decoded.get("frameScriptTargets") or [],
                "waits": decoded.get("waits") or [],
                "sounds": decoded.get("sounds") or [],
                "unknowns": decoded.get("unknowns") or [],
                "decoded": decoded,
            }
        )

    return {
        "version": 1,
        "kind": "hwanse-battle-helper-frame-script-review",
        "source": [
            "Hwanse2.exe",
            "out/battle_helper_child_script_review.json",
        ],
        "status": "helper-frame-script-static-decode",
        "runtimeUsed": False,
        "summary": {
            "frameScriptTargets": len(frame_script_rows),
            "helpersWithFrameScriptTargets": len(helper_to_targets),
            "skillsWithFrameScriptTargets": len(skill_to_targets),
            "targetsWithFrameWrites": sum(1 for row in frame_script_rows if row["frameGateSequence"]),
            "targetsWithInitFrames": sum(1 for row in frame_script_rows if row["initFrameSequence"]),
            "targetsWithRandomRanges": sum(1 for row in frame_script_rows if row["randomRanges"]),
            "targetsWithNestedFrameScriptTargets": sum(1 for row in frame_script_rows if row["nestedFrameScriptTargets"]),
            "targetsWithUnknownOpcode": sum(1 for row in frame_script_rows if row["unknowns"]),
            "opcodeCounts": dict(sorted(opcode_counter.items())),
        },
        "interpretationNotes": [
            "Opcode 0x20 in a helper child script stores this target into display.frameScript(+0x64).",
            "The decoded frame/gate rows are child/effect object frames, not the main actor attack frames.",
            "0x21 writes display.spriteFrame(+0x28) and display.frameGate(+0x62). The gate value is the EXE timing field; the web preview should not replace it with capture-derived timing.",
            "0x08 init blocks can set the first child/effect frame before the frameScript starts ticking.",
            "Some frameScripts spawn or link additional child display objects. Nested frameScript targets are listed but not recursively expanded in the summary table.",
            "No opcode is promoted beyond the existing display-VM evidence here; this report only makes the 0x20 target streams visible and searchable.",
        ],
        "frameScriptRows": frame_script_rows,
        "helperTargetIndex": [
            {"helperId": helper_id, "targetVaHexes": sorted(set(targets))}
            for helper_id, targets in sorted(helper_to_targets.items())
        ],
        "skillTargetIndex": [
            {"skill": skill, "targetVaHexes": sorted(targets)}
            for skill, targets in sorted(skill_to_targets.items())
        ],
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Helper Frame Script Review",
        "",
        f"- status: `{report['status']}`",
        f"- frameScript targets: `{report['summary']['frameScriptTargets']}`",
        f"- helpers with targets: `{report['summary']['helpersWithFrameScriptTargets']}`",
        f"- skills with targets: `{report['summary']['skillsWithFrameScriptTargets']}`",
        f"- targets with unknown opcode: `{report['summary']['targetsWithUnknownOpcode']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend(
        [
            "",
            "## Targets",
            "",
            "| target | helpers | skills | stop | opcodes | init frames | frames@gate | RNG ranges | nested 0x20 |",
            "| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
        ]
    )
    for row in report["frameScriptRows"]:
        frames = vec([item.get("label") for item in row.get("frameGateSequence") or []])
        random_ranges = vec([item.get("randomRange") for item in row.get("randomRanges") or []])
        nested = vec([item.get("targetVaHex") for item in row.get("nestedFrameScriptTargets") or []])
        lines.append(
            f"| `{row['targetVaHex']}` | {vec(row.get('helperIds') or [], '#')} | "
            f"{compact_skills(row.get('skillRows') or [])} | {row.get('stopReason') or '-'} | "
            f"{row.get('opcodeCounts') or {}} | {vec(row.get('initFrameSequence') or [])} | "
            f"{frames} | {random_ranges} | {nested} |"
        )
    return "\n".join(lines) + "\n"


def detail_table(rows: list[dict[str, Any]]) -> str:
    return "".join(
        "<tr>"
        f"<td><code>{esc(row.get('vaHex'))}</code></td>"
        f"<td><code>{esc(row.get('opcode'))}</code></td>"
        f"<td>{esc(row.get('category'))}</td>"
        f"<td>{esc(row.get('length'))}</td>"
        f"<td>{esc(row.get('summary'))}</td>"
        "</tr>"
        for row in rows
    )


def html_page(report: dict[str, Any]) -> str:
    summary_rows = "".join(f"<tr><td>{esc(k)}</td><td><code>{esc(v)}</code></td></tr>" for k, v in report["summary"].items())
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    target_rows = []
    for row in report["frameScriptRows"]:
        skills = "<br>".join(
            f"{esc(skill.get('ownerName'))} {esc(skill.get('skillName'))} <code>{esc(skill.get('skillIdHex'))}</code>"
            for skill in row.get("skillRows") or []
        ) or "-"
        frames = "<br>".join(
            f"<code>{esc(item.get('vaHex'))}</code> sprite {esc(item.get('spriteHex'))} frame {esc(item.get('frame'))} gate {esc(item.get('gate'))}"
            for item in row.get("frameGateSequence") or []
        ) or "-"
        init_frames = esc(vec(row.get("initFrameSequence") or []))
        random_ranges = esc(vec([item.get("randomRange") for item in row.get("randomRanges") or []]))
        nested = esc(vec([item.get("targetVaHex") for item in row.get("nestedFrameScriptTargets") or []]))
        waits = esc(vec([item.get("waitFrames") for item in row.get("waits") or [] if item.get("waitFrames") is not None]))
        opcodes = ", ".join(f"{op}:{count}" for op, count in (row.get("opcodeCounts") or {}).items()) or "-"
        instruction_rows = detail_table((row.get("decoded") or {}).get("rows") or [])
        target_rows.append(
            "<tr>"
            f"<td><code>{esc(row.get('targetVaHex'))}</code></td>"
            f"<td>{esc(vec(row.get('helperIds') or [], '#'))}</td>"
            f"<td>{skills}</td>"
            f"<td>{esc(row.get('stopReason') or '-')}</td>"
            f"<td>{esc(opcodes)}</td>"
            f"<td>{init_frames}</td>"
            f"<td>{frames}</td>"
            f"<td>{random_ranges}</td>"
            f"<td>{waits}</td>"
            f"<td>{nested}</td>"
            f"<td><details><summary>instructions</summary><table><thead><tr><th>VA</th><th>op</th><th>kind</th><th>len</th><th>summary</th></tr></thead><tbody>{instruction_rows}</tbody></table></details></td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="../favicon.ico">
  <title>Battle Helper Frame Script Review</title>
  <style>
    body {{ margin: 20px; background: #101114; color: #f1f3f5; font-family: system-ui, sans-serif; }}
    a {{ color: #9ecbff; }} code {{ color: #ffd37a; }}
    table {{ width: 100%; border-collapse: collapse; margin: 14px 0 24px; }}
    th, td {{ border: 1px solid #30343d; padding: 6px 8px; font-size: 12px; vertical-align: top; }}
    th {{ background: #1a1d24; color: #bac2cf; position: sticky; top: 0; z-index: 2; }}
    tr:nth-child(even) td {{ background: #141820; }}
    details {{ margin: 4px 0; }}
    summary {{ cursor: pointer; color: #ffd37a; }}
    .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; }}
    .panel {{ border: 1px solid #30343d; border-radius: 8px; padding: 12px; background: #151821; }}
    .wide {{ overflow: auto; max-height: 78vh; border: 1px solid #30343d; }}
  </style>
</head>
<body>
  <h1>Battle Helper Frame Script Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_simulator.html">전투 기술 실행</a> · <a href="battle_helper_frame_script_review.json">JSON</a> · <a href="battle_helper_frame_script_review.md">MD</a> · <a href="battle_helper_child_script_review.html">helper child script</a></p>
  <div class="grid">
    <section class="panel"><h2>Summary</h2><table><tbody>{summary_rows}</tbody></table></section>
    <section class="panel"><h2>Interpretation</h2><ul>{notes}</ul></section>
  </div>
  <h2>FrameScript Targets</h2>
  <div class="wide"><table><thead><tr><th>target</th><th>helpers</th><th>skills</th><th>stop</th><th>opcodes</th><th>init frames</th><th>frames/gates</th><th>RNG ranges</th><th>waits</th><th>nested 0x20</th><th>detail</th></tr></thead><tbody>{''.join(target_rows)}</tbody></table></div>
</body>
</html>
"""


def main() -> None:
    report = build()
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "battle_helper_frame_script_review.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    (OUT / "battle_helper_frame_script_review.md").write_text(markdown(report), encoding="utf-8")
    (OUT / "battle_helper_frame_script_review.html").write_text(html_page(report), encoding="utf-8")
    print("wrote out/battle_helper_frame_script_review.{json,md,html}")


if __name__ == "__main__":
    main()
