#!/usr/bin/env python3
"""Group CNS field maps by their source map_* tileset family."""
from __future__ import annotations

import argparse
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"
FAMILY_RE = re.compile(r"^map_([a-z])([0-9]+)$")


def load_json(path: Path, default: Any) -> Any:
    if not path.exists():
        return default
    return json.loads(path.read_text(encoding="utf-8"))


def parse_map_index(path: Path) -> dict[str, dict]:
    text = path.read_text(encoding="utf-8")
    for marker in ("window.HWANSE_MAP_INDEX = ", "window.HWANSE_MAPS = "):
        start = text.find(marker)
        if start >= 0:
            break
    else:
        raise ValueError(f"{path} does not contain Hwanse map data")
    start += len(marker)
    payload = text[start:].strip()
    if payload.endswith(";"):
        payload = payload[:-1]
    return json.loads(payload)


def family_for_tileset(name: str | None) -> str:
    match = FAMILY_RE.match(str(name or ""))
    return match.group(1) if match else "unknown"


def compact_maps(maps: list[dict], limit: int = 8) -> list[dict]:
    return [
        {
            "name": row["name"],
            "width": row.get("width"),
            "height": row.get("height"),
            "layerTilesets": row.get("layerTilesets") or [],
        }
        for row in maps[:limit]
    ]


def build_summary(map_index: dict[str, dict], cns_payloads: list[dict]) -> dict:
    cns_by_stem = {
        str(row.get("name", "")).removesuffix(".cns"): row
        for row in cns_payloads
        if row.get("name")
    }
    source_tilesets: dict[str, list[dict]] = defaultdict(list)
    for stem, row in sorted(cns_by_stem.items()):
        match = FAMILY_RE.match(stem)
        if not match or row.get("kind") != "image":
            continue
        family = match.group(1)
        tile_w = int(row.get("width") or 0) // 16
        tile_h = int(row.get("height") or 0) // 16
        source_tilesets[family].append({
            "stem": stem,
            "name": f"{stem}.cns",
            "webPath": f"../extract_fld/{stem}.cns",
            "width": row.get("width"),
            "height": row.get("height"),
            "bpp": row.get("bpp"),
            "paletteColors": row.get("paletteColors"),
            "tileColumns": tile_w,
            "tileRows": tile_h,
            "tileCount": tile_w * tile_h,
        })

    maps_by_family: dict[str, list[dict]] = defaultdict(list)
    for name, row in sorted(map_index.items()):
        layer_tilesets = row.get("layerTilesets") or []
        family = family_for_tileset(layer_tilesets[0] if layer_tilesets else row.get("tileset"))
        maps_by_family[family].append({
            "name": name,
            "sourceCns": f"{name}.cns",
            "width": row.get("width"),
            "height": row.get("height"),
            "tileCount": int(row.get("width") or 0) * int(row.get("height") or 0),
            "layerTilesets": layer_tilesets,
            "observedSceneTilesets": row.get("observedSceneTilesets") or [],
            "sceneTilesetVariants": row.get("sceneTilesetVariants") or [],
            "sceneIdHex": row.get("sceneIdHex"),
            "sceneRecordVa": row.get("sceneRecordVa"),
        })

    families = []
    for family in sorted(set(source_tilesets) | set(maps_by_family)):
        maps = maps_by_family.get(family, [])
        tilesets = sorted(source_tilesets.get(family, []), key=lambda row: row["stem"])
        selected_pairs = Counter(",".join(row.get("layerTilesets") or []) for row in maps)
        observed_variants = Counter()
        for row in maps:
            for variant in row.get("sceneTilesetVariants") or []:
                observed_variants[",".join(variant)] += 1
        families.append({
            "family": family,
            "label": f"map_{family}*",
            "sourceTilesets": tilesets,
            "sourceTilesetCount": len(tilesets),
            "mapCount": len(maps),
            "maps": maps,
            "sampleMaps": compact_maps(maps),
            "selectedTilesetPairs": [
                {"tilesets": key.split(",") if key else [], "count": count}
                for key, count in selected_pairs.most_common()
            ],
            "observedSceneVariants": [
                {"tilesets": key.split(",") if key else [], "count": count}
                for key, count in observed_variants.most_common()
            ],
        })

    return {
        "scope": "CNS source tileset families and derived tilemap maps",
        "mapCount": sum(len(rows) for rows in maps_by_family.values()),
        "sourceTilesetCount": sum(len(rows) for rows in source_tilesets.values()),
        "familyCount": len(families),
        "families": families,
        "model": {
            "sourceTileset": "map_[family][layer].cns image payload; a 16x16 tile sheet",
            "tilemap": "mapN_NNfamily.cns tilemap payload; width/height plus layer0/layer1 tile index grids",
            "rendering": "tile word value is used as a 0-based index into a 40-column 16x16 tileset sheet in the accepted renderer",
        },
    }


def html_page(summary: dict) -> str:
    data = json.dumps(summary, ensure_ascii=False)
    return "\n".join([
        "<!doctype html>",
        '<html lang="ko">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>CNS Tileset Families</title>",
        "  <style>",
        "    :root{color-scheme:dark;--bg:#101214;--panel:#171a1e;--panel2:#20252b;--line:#303841;--text:#edf0f3;--muted:#aeb7c2;--accent:#7cc7ff;--warn:#ffd166;--good:#7bd88f}",
        "    *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 system-ui,-apple-system,Segoe UI,sans-serif}.topbar{position:sticky;top:0;z-index:5;display:flex;justify-content:space-between;gap:16px;align-items:center;padding:14px 18px;background:#0d0f12;border-bottom:1px solid var(--line)}",
        "    h1{font-size:20px;margin:0}h2{font-size:16px;margin:0 0 10px}h3{font-size:14px;margin:12px 0 8px}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}nav{display:flex;gap:10px;flex-wrap:wrap}.wrap{padding:18px;display:grid;gap:16px}.panel{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px}.filters{display:flex;gap:10px;align-items:center;flex-wrap:wrap}select,input{background:#0f1115;color:var(--text);border:1px solid var(--line);border-radius:6px;padding:8px 10px}.chip{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:999px;padding:2px 8px;background:var(--panel2);color:var(--muted);white-space:nowrap}.chip.good{color:var(--good);border-color:#2f6b3b}.chip.warn{color:var(--warn);border-color:#765f25}.chip.blue{color:var(--accent);border-color:#285c7e}",
        "    .layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:16px}.family-list{display:grid;gap:6px}.family-button{width:100%;border:1px solid var(--line);border-radius:6px;background:#111419;color:var(--text);padding:8px;text-align:left;cursor:pointer}.family-button:hover,.family-button.selected{background:#202b35;border-color:#427aa1}.tilesets{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}.tile-card{border:1px solid var(--line);border-radius:8px;background:#111419;padding:10px}.tile-card canvas{width:100%;height:auto;image-rendering:pixelated;background:#050607;border:1px solid #26313b;border-radius:4px}.map-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:8px}.map-card{border:1px solid var(--line);border-radius:6px;background:#111419;padding:8px}.muted{color:var(--muted)}code{color:#f2d479}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid var(--line);padding:7px 8px;vertical-align:top;text-align:left}th{background:#1d2228;color:#dbe4ee}@media(max-width:900px){.layout{grid-template-columns:1fr}}",
        "  </style>",
        "</head>",
        "<body>",
        "  <div class=\"topbar\"><h1>CNS Tileset Families</h1><nav><a href=\"../web/index.html\">관리 홈</a><a href=\"cns_tilemap_format.json\">tilemap 포맷 JSON</a><a href=\"map_gallery.html\">맵 갤러리</a></nav></div>",
        "  <div class=\"wrap\">",
        "    <section class=\"panel\"><div class=\"filters\"><select id=\"familySelect\"></select><input id=\"search\" type=\"search\" placeholder=\"map1_02b, map_b1, family b\"><span id=\"summary\" class=\"muted\"></span></div></section>",
        "    <main class=\"layout\"><aside class=\"panel\"><h2>Families</h2><div id=\"familyList\" class=\"family-list\"></div></aside><section class=\"panel\"><h2 id=\"detailTitle\">Family</h2><div id=\"detail\"></div></section></main>",
        "  </div>",
        '  <script src="../web/engine/cns/renderer.js"></script>',
        "  <script>",
        f"    const data={data};",
        "    const $=id=>document.getElementById(id);",
        "    const esc=v=>String(v??'').replace(/[&<>\"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[ch]));",
        "    let selectedFamily='';",
        "    function chip(text,cls=''){return `<span class=\"chip ${cls}\">${esc(text)}</span>`}",
        "    function familySearch(f){return [f.family,f.label,(f.sourceTilesets||[]).map(t=>t.stem).join(' '),(f.maps||[]).map(m=>m.name).join(' ')].join(' ').toLowerCase()}",
        "    function visibleFamilies(){const q=$('search').value.trim().toLowerCase();return data.families.filter(f=>!q||familySearch(f).includes(q));}",
        "    function renderList(){const rows=visibleFamilies();$('summary').textContent=`families ${data.familyCount}, source tilesets ${data.sourceTilesetCount}, maps ${data.mapCount}, visible ${rows.length}`;$('familySelect').innerHTML=rows.map(f=>`<option value=\"${esc(f.family)}\">${esc(f.label)} · ${f.mapCount} maps</option>`).join('');$('familyList').innerHTML=rows.map(f=>`<button class=\"family-button ${f.family===selectedFamily?'selected':''}\" data-family=\"${esc(f.family)}\"><b>${esc(f.label)}</b><br><span class=\"muted\">${f.sourceTilesetCount} tilesets · ${f.mapCount} maps</span></button>`).join('')||'<p class=\"muted\">No family.</p>';for(const button of $('familyList').querySelectorAll('button'))button.addEventListener('click',()=>renderFamily(button.dataset.family));if(!rows.some(f=>f.family===selectedFamily)&&rows[0])renderFamily(rows[0].family,false);}",
        "    function tilesetHtml(t){return `<div class=\"tile-card\"><a href=\"${esc(t.webPath)}\"><canvas class=\"tileset-canvas\" data-cns=\"${esc(t.webPath)}\" width=\"${Number(t.width)||1}\" height=\"${Number(t.height)||1}\" aria-label=\"${esc(t.stem)}\"></canvas></a><h3><code>${esc(t.name)}</code></h3><div class=\"muted\">${t.width}x${t.height}px · ${t.tileColumns}x${t.tileRows} tiles · ${t.tileCount} tiles · ${t.bpp}bpp/${t.paletteColors} colors</div></div>`}",
        "    function mapHtml(m){return `<div class=\"map-card\"><a href=\"../web/map_review.html?map=${encodeURIComponent(m.name)}\"><code>${esc(m.name)}</code></a><br><span class=\"muted\">${m.width}x${m.height} · ${m.tileCount} cells</span><br><span>${(m.layerTilesets||[]).map(t=>chip(t,'blue')).join(' ')}</span><div><a href=\"cns_tilemap_format.json\">format JSON</a> · <a href=\"../web/game.html?map=${encodeURIComponent(m.name)}&overview=1\">overview</a></div></div>`}",
        "    function pairsTable(title, rows){if(!rows?.length)return '';return `<h3>${esc(title)}</h3><table><thead><tr><th>tilesets</th><th>maps</th></tr></thead><tbody>${rows.map(r=>`<tr><td>${(r.tilesets||[]).map(t=>`<code>${esc(t)}</code>`).join(', ')}</td><td>${r.count}</td></tr>`).join('')}</tbody></table>`}",
        "    async function renderTilesetCanvases(){for(const canvas of document.querySelectorAll('.tileset-canvas')){try{const image=await window.HWANSE_CNS_RENDERER.loadImageCanvas(canvas.dataset.cns);canvas.width=image.width;canvas.height=image.height;const ctx=canvas.getContext('2d');ctx.imageSmoothingEnabled=false;ctx.clearRect(0,0,canvas.width,canvas.height);ctx.drawImage(image,0,0);}catch(error){canvas.replaceWith(Object.assign(document.createElement('div'),{className:'muted',textContent:`CNS render failed: ${canvas.dataset.cns}`}));}}}",
        "    function renderFamily(family, updateUrl=true){const f=data.families.find(row=>row.family===family)||data.families[0];if(!f)return;selectedFamily=f.family;$('familySelect').value=f.family;$('detailTitle').textContent=`${f.label} source tilesets -> derived maps`;$('detail').innerHTML=`<p class=\"muted\">원천 타일셋 이미지 CNS를 먼저 보고, 아래에서 이 타일셋 family로 조립되는 맵 tilemap CNS를 확인합니다.</p><div>${chip(`${f.sourceTilesetCount} source tilesets`,'good')} ${chip(`${f.mapCount} derived maps`,'blue')}</div><h3>Source Tileset CNS</h3><div class=\"tilesets\">${(f.sourceTilesets||[]).map(tilesetHtml).join('')}</div>${pairsTable('Selected Tileset Pairs',f.selectedTilesetPairs)}${pairsTable('Observed Scene Variants',f.observedSceneVariants)}<h3>Derived Map Tilemap CNS</h3><div class=\"map-grid\">${(f.maps||[]).map(mapHtml).join('')}</div>`;renderTilesetCanvases();if(updateUrl)history.replaceState(null,'',`?family=${encodeURIComponent(f.family)}`);renderListButtonsOnly();}",
        "    function renderListButtonsOnly(){for(const b of $('familyList').querySelectorAll('button'))b.classList.toggle('selected',b.dataset.family===selectedFamily)}",
        "    $('search').addEventListener('input',renderList);$('familySelect').addEventListener('change',e=>renderFamily(e.target.value));const params=new URLSearchParams(location.search);selectedFamily=params.get('family')||'b';renderList();renderFamily(selectedFamily);",
        "  </script>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "cns_tileset_families.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--maps", type=Path, default=OUT / "maps.js")
    parser.add_argument("--cns-payloads", type=Path, default=OUT / "cns_payloads.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        parse_map_index(args.maps),
        load_json(args.cns_payloads, []),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote CNS tileset families summary -> "
        f"{args.out_dir / 'cns_tileset_families.json'} "
        f"({summary['familyCount']} families, {summary['mapCount']} maps)"
    )


if __name__ == "__main__":
    main()
