#!/usr/bin/env python3
"""
将 levels-database.json 烘焙为 Cocos 关卡预制体（.prefab + .prefab.meta）。

预制体结构（对齐 Unity LevelN.prefab）:
  Level{N}
  ├── Border   ← 墙块 Sprite 子节点（纯视觉）
  ├── Ground   ← 地面 Sprite 子节点
  └── (挂 LevelMapData：ground/border 逻辑 JSON)

用法:
  python3 tools/bake_cocos_level_prefabs.py \\
    --db assets/level-data/levels-database.json \\
    --out-dir assets/bundle-level-prefabs/level-prefabs \\
    --limit 0
"""
from __future__ import annotations

import argparse
import json
import random
import re
import string
import uuid
from pathlib import Path

from level_id import LEVEL_ID_BASE, touch_database

LAYER_DEFAULT = 1073741824
LAYER_UI_2D = 33554432  # 编辑器 2D 视图可见（对齐 SpriteSplash.prefab）

CELL = 100

# themes-database.json id → textures/ 子目录（与 ThemeDatabase.getThemeTextureFolder 一致）
THEME_TEXTURE_FOLDER: dict[str, str] = {
    "redarmy": "redArmy",
    "redArmy": "redArmy",
}


def resolve_level_map_data_type() -> str:
    """从 Creator 编译产物读取 LevelMapData 的 __type__（_RF.push 第二参数）。"""
    chunk = Path("temp/programming/packer-driver/targets/preview/chunks/bb/bbf6a3729c922dc4638933a67da37eeb23e90aee.js")
    if chunk.is_file():
        text = chunk.read_text(encoding="utf-8", errors="ignore")
        m = re.search(r'_RF\.push\(\{\},\s*"([^"]+)",\s*"LevelMapData"', text)
        if m:
            return m.group(1)
    return "d4e5fanuMlNDh8qO0xdbn+K"


LEVEL_MAP_DATA_TYPE = resolve_level_map_data_type()
GRID_SNAP_HELPER_TYPE = "739b2brZKNN16CEy+hU2Yo2"
DEFAULT_SPRITE_UUID = "5625da25-9915-416f-be60-c6decb355672@f9941"
TILE_W = 101
TILE_H = 80

# Unity Texture/*.png.meta spritePivot
TILE_PIVOTS: dict[str, tuple[float, float]] = {
    "Baseblock": (0.5, 0.92),
    "JumpBlock": (0.5, 0.77),
    "WallBlock": (0.5, 0.67),
    "kuai11": (0.5, 1.01),
}


def tile_pivot(tile_name: str) -> tuple[float, float]:
    return TILE_PIVOTS.get(tile_name, (0.5, 0.92))


def tile_draw_size(width: int, height: int) -> tuple[float, float]:
    """宽度贴满格子 100px（与 TileSizes.getTileDrawSize 一致）。"""
    scale = CELL / width
    return width * scale, height * scale

def theme_from_cfg(cfg: dict, override: str) -> str:
    if override:
        return override
    t = cfg.get("theme")
    if isinstance(t, str) and t:
        return t
    return "silu"


def texture_folder_for_theme(theme: str) -> str:
    return THEME_TEXTURE_FOLDER.get(theme, theme)


def ground_sprite_path(theme: str, tile_name: str) -> str:
    # Ground 仅地砖
    key = "JumpBlock" if tile_name == "JumpBlock" else "Baseblock"
    return f"textures/{texture_folder_for_theme(theme)}/{key}"


def border_sprite_path(theme: str, tile_val) -> str:
    # Border 仅墙砖/墙饰；地砖名回退 WallBlock
    if tile_val is True or tile_val is None:
        name = "WallBlock"
    elif isinstance(tile_val, str):
        name = tile_val.strip() or "WallBlock"
        if name in ("Baseblock", "JumpBlock"):
            name = "WallBlock"
    else:
        name = "WallBlock"
    return f"textures/{texture_folder_for_theme(theme)}/{name}"


def read_sprite_size(project: Path, texture_path: str) -> tuple[int, int]:
    meta_path = project / "assets/resources" / f"{texture_path}.png.meta"
    if meta_path.is_file():
        try:
            meta = json.loads(meta_path.read_text(encoding="utf-8"))
            ud = meta.get("subMetas", {}).get("f9941", {}).get("userData", {})
            w, h = ud.get("width"), ud.get("height")
            if w and h:
                return int(w), int(h)
        except Exception:
            pass
    return TILE_W, TILE_H


def rebuild_index_from_disk(out: Path) -> dict[str, str]:
    index: dict[str, str] = {}
    for p in sorted(out.glob("Level*.prefab")):
        lid = p.stem.replace("Level", "")
        if lid.isdigit():
            index[lid] = f"level-prefabs/Level{lid}"
    return index


def write_prefab_index(project: Path, out: Path, index: dict[str, str]) -> Path:
    """写入 tools/（非 assets），避免 Cocos 编辑器锁定 _index.json 报 Can not change asset。"""
    index_path = project / "tools" / "level-prefab-index.json"
    index_path.parent.mkdir(parents=True, exist_ok=True)
    index_path.write_text(json.dumps(index, indent=2), encoding="utf-8")
    return index_path


def write_prefab_uuid_index(project: Path, out: Path) -> Path:
    """写入 assets/level-data/level-prefab-uuids.json，供编辑器预览 assetManager.loadAny(uuid)。"""
    uuids: dict[str, str] = {}
    for meta in sorted(out.glob("Level*.prefab.meta")):
        m = re.match(r"Level(\d+)\.prefab\.meta$", meta.name)
        if not m:
            continue
        try:
            data = json.loads(meta.read_text(encoding="utf-8"))
            uid = data.get("uuid")
            if uid:
                uuids[m.group(1)] = uid
        except Exception:
            continue
    uuid_path = project / "assets" / "level-data" / "level-prefab-uuids.json"
    uuid_path.parent.mkdir(parents=True, exist_ok=True)
    uuid_path.write_text(json.dumps(uuids, indent=2), encoding="utf-8")
    return uuid_path


def read_sprite_uuid(project: Path, texture_path: str) -> str:
    meta_path = project / "assets/resources" / f"{texture_path}.png.meta"
    if meta_path.is_file():
        try:
            meta = json.loads(meta_path.read_text(encoding="utf-8"))
            base = meta.get("uuid")
            if base:
                return f"{base}@f9941"
        except Exception:
            pass
    return DEFAULT_SPRITE_UUID


def uid() -> str:
    return str(uuid.uuid4())


def vec3(x: float, y: float, z: float = 0) -> dict:
    return {"__type__": "cc.Vec3", "x": x, "y": y, "z": z}


def quat() -> dict:
    return {"__type__": "cc.Quat", "x": 0, "y": 0, "z": 0, "w": 1}


def cell_pos(x: int, y: int) -> tuple[float, float]:
    """Unity Tilemap tileAnchor (0.5,0.5)：格子中心，CellSize (1,0.5,1)。"""
    half_w = CELL * 0.5
    half_h = CELL * 0.25
    return (x - y) * half_w, (x + y) * half_h + half_h


def file_id() -> str:
    chars = string.ascii_letters + string.digits
    return "".join(random.choices(chars, k=22))


def finalize_prefab_infos(objs: list[dict], root_id: int) -> None:
    """每个节点必须挂 cc.PrefabInfo，否则编辑器 open prefab 会报 instance null。"""
    for obj in objs:
        if obj.get("__type__") != "cc.Node":
            continue
        pid = len(objs)
        objs.append(
            {
                "__type__": "cc.PrefabInfo",
                "root": {"__id__": root_id},
                "asset": {"__id__": 0},
                "fileId": file_id(),
            }
        )
        obj["_prefab"] = {"__id__": pid}


class Builder:
    def __init__(self) -> None:
        self.objs: list[dict] = []

    def add(self, obj: dict) -> int:
        self.objs.append(obj)
        return len(self.objs) - 1

    def comp_prefab_info(self) -> int:
        return self.add({"__type__": "cc.CompPrefabInfo", "fileId": file_id()})

    def node(
        self,
        name: str,
        parent: int | None,
        children: list[int],
        components: list[int],
        x: float,
        y: float,
        layer: int = LAYER_UI_2D,
    ) -> int:
        return self.add(
            {
                "__type__": "cc.Node",
                "_name": name,
                "_objFlags": 0,
                "_parent": {"__id__": parent} if parent is not None else None,
                "_children": [{"__id__": c} for c in children],
                "_active": True,
                "_components": [{"__id__": c} for c in components],
                "_prefab": None,
                "_lpos": vec3(x, y, 0),
                "_lrot": quat(),
                "_lscale": vec3(1, 1, 1),
                "_layer": layer,
                "_euler": vec3(0, 0, 0),
                "_id": "",
            }
        )

    def ui_transform(self, node_id: int, anchor: tuple[float, float], size: tuple[int, int]) -> int:
        cpi = self.comp_prefab_info()
        ax, ay = anchor
        tw, th = size
        ui_id = self.add(
            {
                "__type__": "cc.UITransform",
                "_name": "",
                "_objFlags": 0,
                "node": {"__id__": node_id},
                "_enabled": True,
                "__prefab": {"__id__": cpi},
                "_priority": 0,
                "_contentSize": {"__type__": "cc.Size", "width": tw, "height": th},
                "_anchorPoint": {"__type__": "cc.Vec2", "x": ax, "y": ay},
                "_id": "",
            }
        )
        return ui_id

    def sprite_comp(self, node_id: int, sprite_uuid: str, alpha: int = 255) -> int:
        cpi = self.comp_prefab_info()
        return self.add(
            {
                "__type__": "cc.Sprite",
                "_name": "",
                "_objFlags": 0,
                "node": {"__id__": node_id},
                "_enabled": True,
                "__prefab": {"__id__": cpi},
                "_visFlags": 0,
                "_customMaterial": None,
                "_srcBlendFactor": 2,
                "_dstBlendFactor": 4,
                "_color": {
                    "__type__": "cc.Color",
                    "r": 255,
                    "g": 255,
                    "b": 255,
                    "a": alpha,
                },
                "_spriteFrame": {"__uuid__": sprite_uuid},
                "_type": 0,
                "_fillType": 0,
                "_sizeMode": 0,
                "_fillCenter": {"__type__": "cc.Vec2", "x": 0, "y": 0},
                "_fillStart": 0,
                "_fillRange": 0,
                "_isTrimmedMode": True,
                "_useGrayscale": False,
                "_atlas": None,
                "_id": "",
            }
        )

    def grid_snap_helper(self, node_id: int) -> int:
        cpi = self.comp_prefab_info()
        return self.add(
            {
                "__type__": GRID_SNAP_HELPER_TYPE,
                "_name": "",
                "_objFlags": 0,
                "node": {"__id__": node_id},
                "_enabled": True,
                "__prefab": {"__id__": cpi},
                "snapEnabled": True,
                "showGrid": True,
                "gridRadius": 12,
                "gridPadding": 2,
                "highlightOccupied": True,
                "syncNodeNames": True,
                "_id": "",
            }
        )

    def level_map_data(self, node_id: int, level_id: int, ground: dict, border: dict, theme: str) -> int:
        cpi = self.comp_prefab_info()
        return self.add(
            {
                "__type__": LEVEL_MAP_DATA_TYPE,
                "_name": "",
                "_objFlags": 0,
                "node": {"__id__": node_id},
                "_enabled": True,
                "__prefab": {"__id__": cpi},
                "levelID": level_id,
                "groundJson": json.dumps(ground, ensure_ascii=False, separators=(",", ":")),
                "borderJson": json.dumps(border, ensure_ascii=False, separators=(",", ":")),
                "theme": theme,
                "_id": "",
            }
        )

    def tile(
        self,
        name: str,
        parent: int,
        x: int,
        y: int,
        sprite_uuid: str,
        tile_name: str,
        size: tuple[int, int],
    ) -> int:
        px, py = cell_pos(x, y)
        nid = self.node(name, parent, [], [], px, py, LAYER_UI_2D)
        draw_w, draw_h = tile_draw_size(size[0], size[1])
        ui_id = self.ui_transform(nid, tile_pivot(tile_name), (draw_w, draw_h))
        sp_id = self.sprite_comp(nid, sprite_uuid)
        self.objs[nid]["_components"] = [{"__id__": ui_id}, {"__id__": sp_id}]
        return nid


def build_prefab(level_id: int, cfg: dict, project: Path, theme: str = "") -> list:
    ground = cfg.get("ground") or {}
    border = cfg.get("border") or {}
    theme = theme_from_cfg(cfg, theme)
    b = Builder()
    root_id = 1

    b.add(
        {
            "__type__": "cc.Prefab",
            "_name": f"Level{level_id}",
            "_objFlags": 0,
            "_native": "",
            "data": {"__id__": root_id},
            "optimizationPolicy": 0,
            "persistent": False,
        }
    )
    b.add(
        {
            "__type__": "cc.Node",
            "_name": f"Level{level_id}",
            "_objFlags": 0,
            "_parent": None,
            "_children": [],
            "_active": True,
            "_components": [],
            "_prefab": None,
            "_lpos": vec3(0, 0, 0),
            "_lrot": quat(),
            "_lscale": vec3(1, 1, 1),
            "_layer": LAYER_UI_2D,
            "_euler": vec3(0, 0, 0),
            "_id": "",
        }
    )

    border_tiles: list[int] = []
    for key in sorted(border.keys()):
        xs, ys = key.split(",")
        x, y = int(xs), int(ys)
        val = border[key]
        if val is True or val is None:
            bname = "WallBlock"
        elif isinstance(val, str) and val.strip() and val.strip() not in ("Baseblock", "JumpBlock"):
            bname = val.strip()
        else:
            bname = "WallBlock"
        wall_path = border_sprite_path(theme, bname)
        spr_wall = read_sprite_uuid(project, wall_path)
        border_tiles.append(
            b.tile(f"b_{x}_{y}", -1, x, y, spr_wall, bname, read_sprite_size(project, wall_path))
        )

    ground_tiles: list[int] = []
    for key in sorted(ground.keys()):
        xs, ys = key.split(",")
        x, y = int(xs), int(ys)
        raw = ground[key]
        tile_name = "JumpBlock" if raw == "JumpBlock" else "Baseblock"
        gpath = ground_sprite_path(theme, tile_name)
        spr = read_sprite_uuid(project, gpath)
        ground_tiles.append(
            b.tile(f"g_{x}_{y}", -1, x, y, spr, tile_name, read_sprite_size(project, gpath))
        )

    border_id = b.node("Border", root_id, border_tiles, [], 0, 0, LAYER_UI_2D)
    ground_id = b.node("Ground", root_id, ground_tiles, [], 0, 0, LAYER_UI_2D)
    map_data_id = b.level_map_data(root_id, level_id, ground, border, theme)
    grid_snap_id = b.grid_snap_helper(root_id)
    # Unity 顺序：Ground 在下，Border 在上
    b.objs[root_id]["_children"] = [{"__id__": ground_id}, {"__id__": border_id}]
    b.objs[root_id]["_components"] = [{"__id__": map_data_id}, {"__id__": grid_snap_id}]
    for tid in border_tiles:
        b.objs[tid]["_parent"] = {"__id__": border_id}
    for tid in ground_tiles:
        b.objs[tid]["_parent"] = {"__id__": ground_id}

    finalize_prefab_infos(b.objs, root_id)
    return b.objs


def write_meta(prefab_path: Path) -> None:
    meta_path = prefab_path.with_suffix(".prefab.meta")
    prefab_uuid = uid()
    if meta_path.is_file():
        try:
            existing = json.loads(meta_path.read_text(encoding="utf-8"))
            if existing.get("uuid"):
                prefab_uuid = existing["uuid"]
        except Exception:
            pass
    meta = {
        "ver": "1.1.50",
        "importer": "prefab",
        "imported": True,
        "uuid": prefab_uuid,
        "files": [".json"],
        "subMetas": {},
        "userData": {"syncNodeName": prefab_path.stem},
    }
    meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--db", required=True)
    ap.add_argument("--out-dir", required=True)
    ap.add_argument("--limit", type=int, default=0)
    ap.add_argument("--level-id", type=int, default=0, help="只烘焙指定关卡，0=全部")
    ap.add_argument("--min-level-id", type=int, default=0, help="只烘焙 levelID >= 此值的关卡")
    ap.add_argument("--max-level-id", type=int, default=0, help="只烘焙 levelID <= 此值的关卡")
    ap.add_argument("--rebuild-index", action="store_true", help="仅重建 tools/level-prefab-index.json（不烘焙 prefab）")
    ap.add_argument("--theme", default="", help="贴图主题 silu/sanxing/snow/…，空则从关卡 theme 字段读取")
    args = ap.parse_args()

    project = Path(__file__).resolve().parent.parent
    db = json.loads(Path(args.db).read_text(encoding="utf-8"))
    out = Path(args.out_dir)
    out.mkdir(parents=True, exist_ok=True)

    levels = db["levels"]
    ids = sorted(int(k) for k in levels.keys())
    if args.rebuild_index and args.level_id == 0 and args.min_level_id == 0 and args.max_level_id == 0 and args.limit == 0:
        index = rebuild_index_from_disk(out)
        index_path = write_prefab_index(project, out, index)
        uuid_path = write_prefab_uuid_index(project, out)
        print(f"Index rebuilt -> {index_path} ({len(index)} entries), uuids -> {uuid_path}")
        return

    if args.level_id > 0:
        ids = [args.level_id] if str(args.level_id) in levels else []
    elif args.min_level_id > 0 or args.max_level_id > 0:
        lo = args.min_level_id if args.min_level_id > 0 else min(ids) if ids else 0
        hi = args.max_level_id if args.max_level_id > 0 else max(ids) if ids else 0
        ids = [i for i in ids if lo <= i <= hi]
    elif args.limit > 0:
        ids = ids[: args.limit]

    for lid in ids:
        cfg = levels[str(lid)]
        p = out / f"Level{lid}.prefab"
        p.write_text(json.dumps(build_prefab(lid, cfg, project, args.theme), indent=2), encoding="utf-8")
        write_meta(p)
        if lid % 100 == 0:
            print(f"  baked Level{lid}")

    index = rebuild_index_from_disk(out)
    index_path = write_prefab_index(project, out, index)
    touch_database(Path(args.db), ids)
    write_prefab_uuid_index(project, out)
    print(f"Baked {len(ids)} prefabs -> {out} (index: {len(index)} entries -> {index_path})")


if __name__ == "__main__":
    main()
