#!/usr/bin/env python3
"""Review helper opcodes used by promoted battle display VM scripts.

The static display VM decode already promotes frame, sound, movement, and some
display field writes.  This report focuses on the helper-like instructions that
still need semantic work:

* 0xbd helper ids, currently labelled cleanup/helper in the low-level decode.
* 0x24 cast/effect WLK cues.
* 0x4b display-list cleanup.
* 0x07 child display VM spawn blocks.
* 0x11/0x14 child display writes.

It does not claim full semantics for these opcodes.  It groups them by usage so
that repeated ids and skill families can be promoted with evidence.
"""
from __future__ import annotations

import html
import json
import re
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
DISPLAY_JSON = OUT / "battle_display_vm_static_decode.json"
MOVEMENT_JSON = OUT / "battle_movement_pattern_review.json"


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


def wlk_label(value: Any) -> str:
    return f"WLK id {int(value):02d}"


def wlk_list(values: list[Any]) -> str:
    return ", ".join(wlk_label(value) for value in values) if values else "-"


def helper_id(instr: dict[str, Any]) -> int | None:
    raw = str(instr.get("bytes") or "")
    parts = raw.split()
    if len(parts) >= 3 and parts[0].lower() == "bd":
        try:
            return int(parts[1], 16) | (int(parts[2], 16) << 8)
        except ValueError:
            return None
    match = re.search(r"id=(\d+)", str(instr.get("summary") or ""))
    return int(match.group(1)) if match else None


def key_for(row: dict[str, Any]) -> tuple[str, str]:
    return str(row.get("ownerKey")), str(row.get("skillIdHex")).lower()


def load_movement_index() -> dict[tuple[str, str], dict[str, Any]]:
    if not MOVEMENT_JSON.exists():
        return {}
    data = json.loads(MOVEMENT_JSON.read_text(encoding="utf-8"))
    return {key_for(row): row for row in data.get("rows") or []}


def compact(instr: dict[str, Any]) -> dict[str, Any]:
    result = {
        "vaHex": instr.get("vaHex"),
        "opcode": instr.get("opcode"),
        "category": instr.get("category"),
        "summary": instr.get("summary"),
    }
    for key in (
        "wlkNo",
        "normalWlkNo",
        "altWlkNo",
        "effectArgsHex",
        "targetVaHex",
        "childTargetVaHex",
        "auxHex",
        "destHex",
        "sourceHex",
        "immHex",
        "immFixed",
        "spriteHex",
        "frame",
        "gate",
        "listIndex",
        "maskHex",
        "childObjectType",
    ):
        if key in instr:
            result[key] = instr.get(key)
    if instr.get("category") == "cleanup/helper":
        result["helperId"] = helper_id(instr)
    return {key: value for key, value in result.items() if value is not None}


def row_helpers(decoded: dict[str, Any]) -> list[dict[str, Any]]:
    helpers = []
    for instr in decoded.get("rows") or []:
        category = instr.get("category")
        if category in {"cleanup/helper", "effect-sound", "clear-display-list", "spawn-child-vm", "child-write"}:
            helpers.append(compact(instr))
            continue
        if category == "write" and instr.get("destHex") in {"0x28", "0x80", "0x84", "0x8c", "0x8e", "0x90", "0x92", "0x96", "0xa8"}:
            helpers.append(compact(instr))
            continue
        if instr.get("directFrameSelector"):
            helpers.append(compact(instr))
    return helpers


def helper_role(row: dict[str, Any], helper_ids: list[int], effect_wlks: list[int], clear_lists: list[str], child_targets: list[str], spawn_targets: list[str]) -> str:
    if child_targets:
        return "child-actor/effect-flow"
    if spawn_targets:
        return "spawned-child-display-flow"
    if clear_lists:
        return "display-list-cleanup-flow"
    if helper_ids and effect_wlks:
        return "visual-helper-with-cast-wlk"
    if helper_ids:
        return "visual-helper-id-only"
    if effect_wlks:
        return "cast/effect-wlk-only"
    position = row.get("positionClass") or ""
    if "caster-local" in position or "no-promoted" in position:
        return "no-helper-or-engine-side-effect"
    return "unclassified-helper-pattern"


def build() -> dict[str, Any]:
    display = json.loads(DISPLAY_JSON.read_text(encoding="utf-8"))
    movement_by_key = load_movement_index()
    rows = []
    helper_id_usage: defaultdict[int, list[dict[str, Any]]] = defaultdict(list)
    effect_wlk_usage: defaultdict[int, list[dict[str, Any]]] = defaultdict(list)
    pattern_usage: Counter[str] = Counter()
    clear_list_usage: Counter[str] = Counter()
    child_target_usage: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
    spawn_target_usage: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)

    for decoded in display.get("decodedRows") or []:
        movement = movement_by_key.get(key_for(decoded), {})
        helpers = row_helpers(decoded)
        bd_ids = [item["helperId"] for item in helpers if item.get("category") == "cleanup/helper" and item.get("helperId") is not None]
        effect_wlks = [int(item["wlkNo"]) for item in helpers if item.get("category") == "effect-sound" and item.get("wlkNo")]
        raw_wlks = [int(sound.get("wlkNo")) for sound in decoded.get("sounds") or [] if sound.get("wlkNo")]
        raw_alt_pairs = [
            f"{wlk_label(sound.get('normalWlkNo'))} -> {wlk_label(sound.get('altWlkNo'))}"
            for sound in decoded.get("sounds") or []
            if sound.get("normalWlkNo") and sound.get("altWlkNo") and sound.get("normalWlkNo") != sound.get("altWlkNo")
        ]
        clear_lists = [
            str(item.get("maskHex") or item.get("summary") or "")
            for item in helpers
            if item.get("category") == "clear-display-list"
        ]
        child_targets = sorted({
            str(item.get("childTargetVaHex"))
            for item in helpers
            if item.get("childTargetVaHex")
        })
        spawn_targets = sorted({
            str(item.get("targetVaHex"))
            for item in helpers
            if item.get("category") == "spawn-child-vm" and item.get("targetVaHex")
        })
        position_write_dests = sorted({
            str(item.get("destHex"))
            for item in helpers
            if item.get("destHex")
        })
        usage_label = f"{decoded.get('ownerName')} {decoded.get('skillName')} {decoded.get('skillIdHex')}"
        pattern_key = (
            f"bd={','.join(map(str, bd_ids)) or '-'} | "
            f"0x24={','.join(wlk_label(item) for item in effect_wlks) or '-'} | "
            f"0xc2={','.join(wlk_label(item) for item in raw_wlks) or '-'} | "
            f"clear={','.join(clear_lists) or '-'} | "
            f"child={','.join(child_targets) or '-'}"
        )
        role = helper_role(movement, bd_ids, effect_wlks, clear_lists, child_targets, spawn_targets)
        row = {
            "ownerKey": decoded.get("ownerKey"),
            "ownerName": decoded.get("ownerName"),
            "skillName": decoded.get("skillName"),
            "familyName": decoded.get("familyName"),
            "skillIdHex": decoded.get("skillIdHex"),
            "levelOrFixed": decoded.get("levelOrFixed"),
            "entryStartVaHex": decoded.get("entryStartVaHex"),
            "targetClass": movement.get("targetClass"),
            "positionClass": movement.get("positionClass"),
            "startPlacementPolicy": movement.get("startPlacementPolicy"),
            "helperRole": role,
            "helperIds": bd_ids,
            "effectWlkNos": effect_wlks,
            "rawResultWlkNos": raw_wlks,
            "rawAltPairs": raw_alt_pairs,
            "clearDisplayLists": clear_lists,
            "childTargets": child_targets,
            "spawnChildTargets": spawn_targets,
            "positionWriteDests": position_write_dests,
            "patternKey": pattern_key,
            "helpers": helpers,
        }
        rows.append(row)
        pattern_usage[pattern_key] += 1
        for item in bd_ids:
            helper_id_usage[item].append(row)
        for item in effect_wlks:
            effect_wlk_usage[item].append(row)
        for item in clear_lists:
            clear_list_usage[item] += 1
        for item in child_targets:
            child_target_usage[item].append(row)
        for item in spawn_targets:
            spawn_target_usage[item].append(row)

    def usage_samples(items: list[dict[str, Any]], limit: int = 12) -> list[str]:
        return [f"{item['ownerName']} {item['skillName']} {item['skillIdHex']}" for item in items[:limit]]

    helper_groups = [
        {
            "helperId": helper_id_value,
            "count": len(items),
            "owners": sorted({str(item["ownerName"]) for item in items}),
            "skillNames": sorted({str(item["skillName"]) for item in items}),
            "families": sorted({str(item.get("familyName") or "") for item in items if item.get("familyName")}),
            "positionClasses": sorted({str(item.get("positionClass") or "") for item in items if item.get("positionClass")}),
            "effectWlkNos": sorted({wlk for item in items for wlk in item["effectWlkNos"]}),
            "rawResultWlkNos": sorted({wlk for item in items for wlk in item["rawResultWlkNos"]}),
            "samples": usage_samples(items),
        }
        for helper_id_value, items in sorted(helper_id_usage.items())
    ]
    effect_wlk_groups = [
        {
            "wlkNo": wlk_no,
            "count": len(items),
            "skillNames": sorted({str(item["skillName"]) for item in items}),
            "helperIds": sorted({helper_id_value for item in items for helper_id_value in item["helperIds"]}),
            "samples": usage_samples(items),
        }
        for wlk_no, items in sorted(effect_wlk_usage.items())
    ]
    child_target_groups = [
        {
            "targetVaHex": target,
            "count": len(items),
            "skillNames": sorted({str(item["skillName"]) for item in items}),
            "samples": usage_samples(items),
        }
        for target, items in sorted(child_target_usage.items())
    ]
    return {
        "version": 1,
        "kind": "hwanse-battle-helper-opcode-review",
        "source": ["out/battle_display_vm_static_decode.json", "out/battle_movement_pattern_review.json"],
        "status": "groups-helper-like-battle-vm-opcodes",
        "summary": {
            "decodedRows": len(rows),
            "rowsWithHelperId0xbd": sum(1 for row in rows if row["helperIds"]),
            "uniqueHelperIds0xbd": len(helper_id_usage),
            "rowsWithEffectWlk0x24": sum(1 for row in rows if row["effectWlkNos"]),
            "uniqueEffectWlk0x24": len(effect_wlk_usage),
            "rowsWithClearDisplayList0x4b": sum(1 for row in rows if row["clearDisplayLists"]),
            "clearDisplayListPatterns0x4b": dict(clear_list_usage),
            "rowsWithChildTargets": sum(1 for row in rows if row["childTargets"]),
            "uniqueChildTargets": len(child_target_usage),
            "rowsWithSpawnChildVm0x07": sum(1 for row in rows if row["spawnChildTargets"]),
            "uniquePatternKeys": len(pattern_usage),
            "helperRoleCounts": dict(Counter(row["helperRole"] for row in rows)),
        },
        "interpretationNotes": [
            "0xbd is no longer treated as a pure cleanup clue in this review. It is grouped as a visual/helper id candidate because many ids scale with skill level or visual family.",
            "0x24 remains a cast/effect WLK cue. It should be rendered separately from 0xc2 result sounds.",
            "0x4b is display-list cleanup/free by list slot and mask, not a visual resource/effect helper.",
            "0x07 spawns a child display VM object and stores it in +0x58; it is not a switch/control opcode.",
            "0x11/0x14 child writes are strongly concentrated in 인법·분신술 and point to child actor/effect flows using sprite 0x17 in the child decode.",
        ],
        "rows": rows,
        "helperIdGroups": helper_groups,
        "effectWlkGroups": effect_wlk_groups,
        "childTargetGroups": child_target_groups,
        "patternGroups": [
            {"patternKey": key, "count": count}
            for key, count in pattern_usage.most_common()
        ],
        "spawnTargetGroups": [
            {"targetVaHex": target, "count": len(items), "samples": usage_samples(items)}
            for target, items in sorted(spawn_target_usage.items())
        ],
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Helper Opcode Review",
        "",
        f"- status: `{report['status']}`",
        f"- decoded rows: `{report['summary']['decodedRows']}`",
        f"- unique 0xbd helper ids: `{report['summary']['uniqueHelperIds0xbd']}`",
        f"- unique 0x24 WLK cues: `{report['summary']['uniqueEffectWlk0x24']}`",
        f"- unique child targets: `{report['summary']['uniqueChildTargets']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend([
        "",
        "## 0xbd Helper Id Groups",
        "",
        "| id | count | skills | effect WLK | result WLK | samples |",
        "| ---: | ---: | --- | --- | --- | --- |",
    ])
    for group in report["helperIdGroups"]:
        lines.append(
            f"| {group['helperId']} | {group['count']} | {', '.join(group['skillNames'])} | "
            f"{wlk_list(group['effectWlkNos'])} | "
            f"{wlk_list(group['rawResultWlkNos'])} | "
            f"{'; '.join(group['samples'])} |"
        )
    lines.extend([
        "",
        "## Skill Rows",
        "",
        "| actor | skill | id | role | position | 0xbd | 0x24 | 0xc2 | child/cleanup/spawn |",
        "| --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for row in report["rows"]:
        if not (row["helperIds"] or row["effectWlkNos"] or row["clearDisplayLists"] or row["childTargets"] or row["spawnChildTargets"]):
            continue
        misc = []
        if row["childTargets"]:
            misc.append("child " + ", ".join(row["childTargets"]))
        if row["clearDisplayLists"]:
            misc.append("clear " + ", ".join(row["clearDisplayLists"]))
        if row["spawnChildTargets"]:
            misc.append("spawn " + ", ".join(row["spawnChildTargets"]))
        lines.append(
            f"| {row['ownerName']} | {row['skillName']} | `{row['skillIdHex']}` | `{row['helperRole']}` | "
            f"{row.get('positionClass') or '-'} / {row.get('startPlacementPolicy') or '-'} | "
            f"{', '.join(map(str, row['helperIds'])) or '-'} | "
            f"{wlk_list(row['effectWlkNos'])} | "
            f"{wlk_list(row['rawResultWlkNos'])} | "
            f"{'; '.join(misc) or '-'} |"
        )
    return "\n".join(lines) + "\n"


def html_page(report: dict[str, Any]) -> str:
    summary_rows = "".join(
        f"<tr><td>{esc(key)}</td><td><code>{esc(value)}</code></td></tr>"
        for key, value in report["summary"].items()
    )
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    helper_rows = []
    for group in report["helperIdGroups"]:
        helper_rows.append(
            "<tr>"
            f"<td><code>{esc(group['helperId'])}</code></td>"
            f"<td>{esc(group['count'])}</td>"
            f"<td>{esc(', '.join(group['skillNames']))}</td>"
            f"<td>{esc(', '.join(group['families']))}</td>"
            f"<td>{esc(wlk_list(group['effectWlkNos']))}</td>"
            f"<td>{esc(wlk_list(group['rawResultWlkNos']))}</td>"
            f"<td>{esc('; '.join(group['samples']))}</td>"
            "</tr>"
        )
    effect_rows = []
    for group in report["effectWlkGroups"]:
        effect_rows.append(
            "<tr>"
            f"<td><code>{esc(wlk_label(group['wlkNo']))}</code></td>"
            f"<td>{esc(group['count'])}</td>"
            f"<td>{esc(', '.join(group['skillNames']))}</td>"
            f"<td>{esc(', '.join(map(str, group['helperIds'])) or '-')}</td>"
            f"<td>{esc('; '.join(group['samples']))}</td>"
            "</tr>"
        )
    skill_rows = []
    for row in report["rows"]:
        if not (row["helperIds"] or row["effectWlkNos"] or row["clearDisplayLists"] or row["childTargets"] or row["spawnChildTargets"]):
            continue
        misc = []
        if row["childTargets"]:
            misc.append("child " + ", ".join(row["childTargets"]))
        if row["clearDisplayLists"]:
            misc.append("clear " + ", ".join(row["clearDisplayLists"]))
        if row["spawnChildTargets"]:
            misc.append("spawn " + ", ".join(row["spawnChildTargets"]))
        helper_list = "<br>".join(
            f"<code>{esc(item.get('vaHex'))}</code> {esc(item.get('category'))}: {esc(item.get('summary'))}"
            for item in row["helpers"]
        )
        skill_rows.append(
            "<tr>"
            f"<td>{esc(row['ownerName'])}</td>"
            f"<td>{esc(row['skillName'])}<br><code>{esc(row['skillIdHex'])}</code></td>"
            f"<td><code>{esc(row['helperRole'])}</code></td>"
            f"<td>{esc(row.get('positionClass') or '-')}<br>{esc(row.get('startPlacementPolicy') or '-')}</td>"
            f"<td>{esc(', '.join(map(str, row['helperIds'])) or '-')}</td>"
            f"<td>{esc(wlk_list(row['effectWlkNos']))}</td>"
            f"<td>{esc(wlk_list(row['rawResultWlkNos']))}<br>{esc(', '.join(row['rawAltPairs']) or '-')}</td>"
            f"<td>{esc('; '.join(misc) or '-')}</td>"
            f"<td>{helper_list}</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 Opcode 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; }}
    .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 Opcode Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_simulator.html">전투 기술 실행</a> · <a href="battle_display_vm_static_decode.html">VM 정적 디코드</a> · <a href="battle_sound_effect_review.html">사운드/이펙트</a> · <a href="battle_effect_object_review.html">이펙트 객체</a> · <a href="battle_helper_opcode_review.json">JSON</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>0xbd Helper Id Groups</h2>
  <div class="wide"><table><thead><tr><th>id</th><th>count</th><th>skills</th><th>families</th><th>0x24 WLK</th><th>0xc2 WLK</th><th>samples</th></tr></thead><tbody>{''.join(helper_rows)}</tbody></table></div>
  <h2>0x24 Effect WLK Groups</h2>
  <div class="wide"><table><thead><tr><th>WLK</th><th>count</th><th>skills</th><th>helper ids</th><th>samples</th></tr></thead><tbody>{''.join(effect_rows)}</tbody></table></div>
  <h2>Skill Rows</h2>
  <div class="wide"><table><thead><tr><th>actor</th><th>skill</th><th>role</th><th>position</th><th>0xbd</th><th>0x24</th><th>0xc2</th><th>child/screen/control</th><th>helpers</th></tr></thead><tbody>{''.join(skill_rows)}</tbody></table></div>
</body>
</html>
"""


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


if __name__ == "__main__":
    main()
