#!/usr/bin/env python3
"""Asset edit of Tim's "Animated Female Base Mesh" for the bundled default
female (design.md, t1018u/t1020u): drop the stray animation that resets the
armature rotation and the hair mesh (material ponytail01) with its material,
texture, image and data, then compact the .bin. Usage: SRC_DIR DST_DIR"""
import json, os, shutil, sys

src, dst = sys.argv[1], sys.argv[2]
g = json.load(open(os.path.join(src, "female.gltf")))
blob = open(os.path.join(src, g["buffers"][0]["uri"]), "rb").read()

g["animations"] = [a for a in g["animations"] if "ponytail" not in a["name"]]
hair_mat = next(i for i, m in enumerate(g["materials"]) if m.get("name") == "ponytail01")
hair_meshes = {i for i, m in enumerate(g["meshes"]) if any(p.get("material") == hair_mat for p in m["primitives"])}
assert len(hair_meshes) == 1 and all(p.get("material") == hair_mat for p in g["meshes"][next(iter(hair_meshes))]["primitives"])
for n in g["nodes"]:
    if n.get("mesh") in hair_meshes:
        del n["mesh"]
        n.pop("skin", None)

def remap(items, keep):
    new = {old: i for i, old in enumerate(keep)}
    return [items[i] for i in keep], new

# meshes
keep = [i for i in range(len(g["meshes"])) if i not in hair_meshes]
g["meshes"], m_map = remap(g["meshes"], keep)
for n in g["nodes"]:
    if "mesh" in n: n["mesh"] = m_map[n["mesh"]]
# materials / textures / images
keep = [i for i in range(len(g["materials"])) if i != hair_mat]
used_tex = set()
def textures_of(m):
    out = []
    def walk(o):
        if isinstance(o, dict):
            if "index" in o and set(o) & {"index"} and ("texCoord" in o or len(o) <= 3): out.append(o)
            for v in o.values(): walk(v)
        elif isinstance(o, list):
            for v in o: walk(v)
    walk(m); return out
g["materials"], mat_map = remap(g["materials"], keep)
for m in g["meshes"]:
    for p in m["primitives"]:
        if "material" in p: p["material"] = mat_map[p["material"]]
refs = [r for m in g["materials"] for r in textures_of(m)]
tkeep = sorted({r["index"] for r in refs})
g["textures"], t_map = remap(g["textures"], tkeep)
for r in refs: r["index"] = t_map[r["index"]]
ikeep = sorted({t["source"] for t in g["textures"]})
dropped_images = [g["images"][i]["uri"] for i in range(len(g["images"])) if i not in ikeep]
g["images"], i_map = remap(g["images"], ikeep)
for t in g["textures"]: t["source"] = i_map[t["source"]]
# accessors still referenced
used_acc = set()
for m in g["meshes"]:
    for p in m["primitives"]:
        used_acc |= set(p["attributes"].values())
        if "indices" in p: used_acc.add(p["indices"])
        for t in p.get("targets", []): used_acc |= set(t.values())
for s in g.get("skins", []):
    if "inverseBindMatrices" in s: used_acc.add(s["inverseBindMatrices"])
for a in g["animations"]:
    for s in a["samplers"]: used_acc |= {s["input"], s["output"]}
akeep = sorted(used_acc)
g["accessors"], a_map = remap(g["accessors"], akeep)
for m in g["meshes"]:
    for p in m["primitives"]:
        p["attributes"] = {k: a_map[v] for k, v in p["attributes"].items()}
        if "indices" in p: p["indices"] = a_map[p["indices"]]
for s in g.get("skins", []):
    if "inverseBindMatrices" in s: s["inverseBindMatrices"] = a_map[s["inverseBindMatrices"]]
for a in g["animations"]:
    for s in a["samplers"]: s["input"], s["output"] = a_map[s["input"]], a_map[s["output"]]
# compact bin: every kept accessor gets its own tightly packed buffer view
# (the source packs all meshes into a few shared views).
comp = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4}
ncomp = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}
assert not any("bufferView" in im for im in g["images"])
out = bytearray(); views = []
for a in g["accessors"]:
    if "bufferView" not in a: continue
    v = g["bufferViews"][a["bufferView"]]
    size = comp[a["componentType"]] * ncomp[a["type"]]
    stride = v.get("byteStride", size)
    base = v.get("byteOffset", 0) + a.get("byteOffset", 0)
    data = b"".join(blob[base + k * stride: base + k * stride + size] for k in range(a["count"]))
    while len(out) % 4: out.append(0)
    nv = {"buffer": 0, "byteOffset": len(out), "byteLength": len(data)}
    if "target" in v: nv["target"] = v["target"]
    views.append(nv); out += data
    a["bufferView"] = len(views) - 1; a.pop("byteOffset", None)
g["bufferViews"] = views
g["buffers"][0]["byteLength"] = len(out)
g["buffers"][0]["uri"] = "female.bin"
os.makedirs(os.path.join(dst, "textures"), exist_ok=True)
json.dump(g, open(os.path.join(dst, "female.gltf"), "w"), indent=2)
open(os.path.join(dst, "female.bin"), "wb").write(out)
for im in g["images"]:
    shutil.copyfile(os.path.join(src, im["uri"]), os.path.join(dst, im["uri"]))
for im in dropped_images:
    p = os.path.join(dst, im)
    if os.path.exists(p): os.remove(p)
shutil.copyfile(os.path.join(src, "license.txt"), os.path.join(dst, "license.txt"))
print("bin", len(blob), "->", len(out), "dropped images", dropped_images)
