#!/usr/bin/env python3
"""Summarize EXE-grounded sparkle helper motion facts.

This report intentionally separates values decoded from Hwanse2.exe from
preview-only coordinates used by the browser timeline page.  The visible
sparkle family is shared by mode-4 actor movement and Smashu's higher-level
쾌진격 helpers.
"""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"


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


def load_json(name: str) -> dict[str, Any]:
    return json.loads((OUT / name).read_text(encoding="utf-8"))


def compact(values: list[Any], limit: int = 10) -> str:
    values = list(values)
    shown = values[:limit]
    text = ", ".join(str(value) for value in shown)
    if len(values) > limit:
        text += f", ... +{len(values) - limit}"
    return text or "-"


def first_step_count(decoded: dict[str, Any]) -> dict[str, Any] | None:
    for write in decoded.get("initWrites") or []:
        if write.get("destHex") == "0x96":
            return {
                "vaHex": write.get("vaHex"),
                "destHex": write.get("destHex"),
                "destLabel": write.get("destLabel"),
                "value": write.get("value"),
                "valueHex": write.get("valueHex"),
                "summary": write.get("summary"),
            }
    return None


def row_by_va(rows: list[dict[str, Any]], va_hex: str) -> dict[str, Any] | None:
    return next((row for row in rows if str(row.get("vaHex")).lower() == va_hex.lower()), None)


def child_rows_of_interest(decoded: dict[str, Any]) -> list[dict[str, Any]]:
    wanted = {
        "0x004b502c",
        "0x004b5030",
        "0x004b5044",
        "0x004b5064",
        "0x004b5078",
        "0x004b508c",
        "0x004b50a8",
        "0x004b50dc",
        "0x004b50f0",
        "0x004b50f8",
        "0x004b5104",
        "0x004b510c",
        "0x004b5138",
        "0x004b5164",
        "0x004b517c",
    }
    out: list[dict[str, Any]] = []
    for row in decoded.get("rows") or []:
        if str(row.get("vaHex")).lower() not in wanted:
            continue
        out.append(
            {
                "vaHex": row.get("vaHex"),
                "opcode": row.get("opcode"),
                "category": row.get("category"),
                "summary": row.get("summary"),
                "targetVaHex": row.get("targetVaHex"),
                "randomRange": row.get("randomRange"),
                "randomRangeHex": row.get("randomRangeHex"),
                "comparison": row.get("comparison"),
            }
        )
    return out


def qaejin_rows() -> list[dict[str, Any]]:
    child_report = load_json("battle_helper_child_script_review.json")
    skill_report = load_json("battle_skill_complete_pattern_review.json")
    skill_by_helper: dict[int, dict[str, Any]] = {}
    for row in skill_report.get("rows") or []:
        if row.get("ownerKey") != "smashu" or row.get("skillName") != "쾌진격":
            continue
        for helper_id in row.get("helperIds") or []:
            skill_by_helper[int(helper_id)] = row

    rows: list[dict[str, Any]] = []
    for helper in child_report.get("helperRows") or []:
        helper_id = int(helper.get("helperId") or -1)
        if helper_id not in {29, 30, 31}:
            continue
        decoded = helper.get("decoded") or {}
        step = first_step_count(decoded)
        skill = skill_by_helper.get(helper_id) or {}
        rows.append(
            {
                "ownerKey": "smashu",
                "ownerName": "스마슈",
                "skillName": "쾌진격",
                "skillIdHex": skill.get("skillIdHex") or ((helper.get("skillRows") or [{}])[0].get("skillIdHex")),
                "level": skill.get("levelOrFixed"),
                "key": f"smashu:{str(skill.get('skillIdHex') or '').lower()}",
                "helperId": helper_id,
                "childScriptVaHex": helper.get("childScriptVaHex"),
                "stepCount": step.get("value") if step else None,
                "stepCountWrite": step,
                "spawnTargetVaHex": "0x004b5138",
                "frameScriptVaHex": "0x004b517c",
                "frameSequence": list(range(30, 38)),
                "gateSequence": [2] * 8,
                "randomRanges": decoded.get("randomRanges") or [],
                "spawnRows": decoded.get("spawnRows") or [],
                "branchTargets": [
                    branch
                    for branch in decoded.get("branchTargets") or []
                    if branch.get("targetVaHex") in {"0x004b5030", "0x004b502c"}
                ],
                "rowsOfInterest": child_rows_of_interest(decoded),
                "exeConfirmed": [
                    "helper child script VA",
                    "motion.stepCount +0x96 initial value",
                    "spawn child display VM 0x004b5138",
                    "branch 0x004b50f8 loops to 0x004b5030 while +0x90 != 0",
                    "random ranges and trig-motion opcodes used inside the loop",
                    "child frame script 0x004b517c emits btl_efc#30..37 gate 2",
                ],
                "previewOnly": [
                    "exact runtime RNG seed",
                    "exact screen-space particle coordinates",
                    "browser particle scatter visualization",
                ],
            }
        )
    return sorted(rows, key=lambda row: int(row["helperId"]))


def mode4_summary() -> dict[str, Any]:
    report = load_json("battle_mouko_special_sparkle_review.json")
    evidence = report.get("mode4Evidence") or {}
    summary = report.get("summary") or {}
    return {
        "branchVaHex": evidence.get("mode4BranchStartVaHex"),
        "childStartVaHex": evidence.get("childStartVaHex"),
        "childContextVaHex": evidence.get("childContextVaHex"),
        "frameSequence": summary.get("sharedFrameSequence") or [30, 31, 32, 33, 34, 35, 36, 37],
        "gateSequence": summary.get("sharedGateSequence") or [2] * 8,
        "childSpawnTotalCount": summary.get("childSpawnTotalCount"),
        "repeatLoopCount": summary.get("childSpawnRepeatCount"),
        "visibleSkillKeys": [
            {
                "ownerKey": row.get("ownerKey"),
                "skillName": row.get("skillName"),
                "skillIdHex": row.get("skillIdHex"),
                "level": row.get("level"),
                "key": row.get("key"),
                "tick": row.get("tick"),
            }
            for row in summary.get("visibleSparkleCalls") or []
        ],
        "branchRowsOfInterest": evidence.get("branchRowsOfInterest") or [],
        "frameFragmentReview": evidence.get("frameFragmentReview") or [],
        "exeConfirmed": [
            "0xbc movementMode=4 branch target 0x004b5b4c",
            "0x004b5b4c spawns child display VM 0x004b5138",
            "0x004b517c emits btl_efc#30..37 gate 2",
            "0x004b5cac repeat count is total child body executions",
        ],
        "previewOnly": [
            "exact runtime RNG seed",
            "exact target-side screen coordinates",
        ],
    }


def build() -> dict[str, Any]:
    qaejin = qaejin_rows()
    mode4 = mode4_summary()
    return {
        "version": 1,
        "kind": "battle-sparkle-motion-review",
        "status": "static-exe-sparkle-loop-count-and-shared-child-frame-script",
        "source": [
            "out/battle_helper_child_script_review.json",
            "out/battle_skill_complete_pattern_review.json",
            "out/battle_mouko_special_sparkle_review.json",
            "Hwanse2.exe display VM scripts 0x004b4fac/0x004b5138/0x004b517c/0x004b5b4c",
        ],
        "summary": {
            "mode4ChildSpawnTotalCount": mode4.get("childSpawnTotalCount"),
            "qaejinHelperStepCounts": [
                {
                    "skillIdHex": row.get("skillIdHex"),
                    "level": row.get("level"),
                    "helperId": row.get("helperId"),
                    "stepCount": row.get("stepCount"),
                }
                for row in qaejin
            ],
            "sharedSparkleFrames": mode4.get("frameSequence"),
            "sharedSparkleGates": mode4.get("gateSequence"),
        },
        "interpretationNotes": [
            "mode4 계열과 쾌진격 helper 29/30/31은 모두 0x004b5138 child와 0x004b517c frame script를 공유한다.",
            "쾌진격 2/3/4단은 helper init에서 +0x96 stepCount가 각각 1/2/10으로 들어간다. 따라서 신기 쾌진격을 4개 고정 파티클로 보는 것은 EXE 근거와 맞지 않는다.",
            "정적 EXE로 확정 가능한 것은 helper 반복 수, child frame stream, gate, RNG range, branch 구조다. 런타임 RNG seed와 최종 화면 좌표는 아직 preview projection으로만 표현한다.",
            "btl_efc#30..37 내부의 여러 밝은 점은 CNS 프레임 픽셀/컴포넌트이며, EXE가 한 child에서 여러 sprite copy를 추가로 그린다는 증거는 아직 없다.",
        ],
        "mode4": mode4,
        "qaejin": qaejin,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Sparkle Motion Review",
        "",
        f"- status: `{report['status']}`",
        f"- mode4 child total: `{report['summary']['mode4ChildSpawnTotalCount']}`",
        f"- shared frames: `{compact(report['summary']['sharedSparkleFrames'])}`",
        f"- shared gates: `{compact(report['summary']['sharedSparkleGates'])}`",
        "",
        "## 쾌진격 Helper",
        "",
        "| skill | id | lv | helper | child script | stepCount | spawn | frame script |",
        "|---|---:|---:|---:|---|---:|---|---|",
    ]
    for row in report["qaejin"]:
        lines.append(
            "| "
            + " | ".join(
                [
                    str(row.get("skillName")),
                    str(row.get("skillIdHex")),
                    str(row.get("level")),
                    str(row.get("helperId")),
                    str(row.get("childScriptVaHex")),
                    str(row.get("stepCount")),
                    str(row.get("spawnTargetVaHex")),
                    str(row.get("frameScriptVaHex")),
                ]
            )
            + " |"
        )
    lines += [
        "",
        "## 확정/미확정",
        "",
        "- EXE 확정: helper stepCount, loop branch, child spawn target, btl_efc#30..37@gate2.",
        "- 미확정: 런타임 RNG seed, 최종 화면 좌표, 브라우저 preview scatter 배치.",
        "",
        "## Notes",
    ]
    lines += [f"- {note}" for note in report["interpretationNotes"]]
    return "\n".join(lines) + "\n"


def html_page(report: dict[str, Any]) -> str:
    rows = "\n".join(
        "<tr>"
        f"<td>{esc(row.get('skillName'))}</td>"
        f"<td><code>{esc(row.get('skillIdHex'))}</code></td>"
        f"<td>{esc(row.get('level'))}</td>"
        f"<td>{esc(row.get('helperId'))}</td>"
        f"<td><code>{esc(row.get('childScriptVaHex'))}</code></td>"
        f"<td>{esc(row.get('stepCount'))}</td>"
        f"<td><code>{esc(row.get('spawnTargetVaHex'))}</code></td>"
        f"<td><code>{esc(row.get('frameScriptVaHex'))}</code></td>"
        f"<td>{esc(compact([r.get('randomRangeHex') for r in row.get('randomRanges') or []]))}</td>"
        "</tr>"
        for row in report["qaejin"]
    )
    mode4_rows = "\n".join(
        "<tr>"
        f"<td><code>{esc(row.get('vaHex'))}</code></td>"
        f"<td>{esc(row.get('opcode'))}</td>"
        f"<td>{esc(row.get('category'))}</td>"
        f"<td>{esc(row.get('summary'))}</td>"
        "</tr>"
        for row in report["mode4"].get("branchRowsOfInterest") or []
    )
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Battle Sparkle Motion Review</title>
<style>
body {{ font-family: system-ui, sans-serif; margin: 24px; background: #f7f4ee; color: #221f1a; }}
table {{ border-collapse: collapse; width: 100%; background: #fff; margin: 16px 0; }}
th, td {{ border: 1px solid #ddd2bf; padding: 6px 8px; text-align: left; vertical-align: top; }}
th {{ background: #efe5d2; }}
code {{ background: #f1ece4; padding: 1px 4px; border-radius: 3px; }}
.pill {{ display: inline-block; padding: 3px 8px; background: #1f7a57; color: #fff; border-radius: 999px; margin-right: 6px; }}
</style>
</head>
<body>
<h1>Battle Sparkle Motion Review</h1>
<p><span class="pill">{esc(report['status'])}</span></p>
<p>mode4 child total: <code>{esc(report['summary']['mode4ChildSpawnTotalCount'])}</code>,
shared frames: <code>{esc(compact(report['summary']['sharedSparkleFrames']))}</code>,
shared gates: <code>{esc(compact(report['summary']['sharedSparkleGates']))}</code></p>
<h2>쾌진격 Helper</h2>
<table><thead><tr><th>skill</th><th>id</th><th>lv</th><th>helper</th><th>child script</th><th>stepCount</th><th>spawn</th><th>frame script</th><th>rng ranges</th></tr></thead><tbody>{rows}</tbody></table>
<h2>mode4 Branch Rows</h2>
<table><thead><tr><th>VA</th><th>op</th><th>category</th><th>summary</th></tr></thead><tbody>{mode4_rows}</tbody></table>
<h2>Notes</h2>
<ul>{notes}</ul>
</body>
</html>
"""


def main() -> None:
    report = build()
    (OUT / "battle_sparkle_motion_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (OUT / "battle_sparkle_motion_review.md").write_text(markdown(report), encoding="utf-8")
    (OUT / "battle_sparkle_motion_review.html").write_text(html_page(report), encoding="utf-8")


if __name__ == "__main__":
    main()
