#!/usr/bin/env python

import os, sys, time
import bpy
from math import pi
import glob
import shutil
from mathutils import Vector, Matrix
from math import radians, pi
import json

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
#from settings import ExportMappingFemale, ExportMappingMale, ExportMappingMaleBabyShape, ExportMappingMaleEdited, ExportMappingFemaleEdited, ExportMappingMaleTestShapeEdited, ExportMappingMaleBaseShapeEdited

argv = sys.argv
argv = argv[argv.index("--") + 1:]


basepath = os.getcwd()
def check_bone(bname):
    ok = True
    baddie = ["ctrl_", "mch_", "MCH_", "Ctrl_", "Mch_"]
    for bd in baddie:
        if bname.startswith(bd):
            ok = False
            break
    return ok

def prepare_armature(mapping):
    print("Preparing...")
    bpy.ops.object.armature_add(enter_editmode=False)
    new_armature = bpy.context.object
    orig_armature = bpy.data.objects[mapping.armature_name]
    armature_name = orig_armature.name
    orig_armature.name = orig_armature.name + "_orig"
    new_armature.name = armature_name
    queue = []
    if new_armature.animation_data is None:
        new_armature.animation_data_create()
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='EDIT')
    for b in new_armature.data.edit_bones:
        new_armature.data.edit_bones.remove(b)
    bpy.context.view_layer.objects.active = orig_armature
    bpy.ops.object.mode_set(mode='EDIT')
    for b in orig_armature.data.edit_bones:
        print(b.name)
        if b.parent is None:
            queue.append(b.name)
    print("Copying bones...")
    while len(queue) > 0:
        item = queue.pop(0)
        print(item)
        itemb = orig_armature.data.edit_bones[item]
        if not itemb.use_deform and not check_bone(item):
            continue
        for cb in orig_armature.data.edit_bones:
            if cb.parent == itemb:
                queue.append(cb.name)
        nb = new_armature.data.edit_bones.new(item)
        nb.name = item
        nb.head = itemb.head
        nb.tail = itemb.tail
        nb.matrix = itemb.matrix
        nb.use_deform = itemb.use_deform
        if itemb.parent is not None:
            ok = True
            pname = itemb.parent.name
            while not check_bone(pname):
                bparent = itemb.parent.parent
                if bparent is None:
                    ok = False
                    break
                pname = bparent.name
            if ok:
                nb.parent = new_armature.data.edit_bones[itemb.parent.name]
            else:
                nb.parent = None
        else:
            nb.parent = None
        nb.use_connect = itemb.use_connect
#    drivers_data = new_armature.animation_data.drivers
    print("Creating constraints...")
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.context.view_layer.objects.active = orig_armature
    bpy.ops.object.mode_set(mode='OBJECT')
    for b in new_armature.pose.bones:
        print(b.name)
        c = b.constraints.new(type='COPY_TRANSFORMS')
        c.target = orig_armature
        c.subtarget = b.name
    for obj in bpy.data.objects:
        if obj.parent == orig_armature:
            obj.parent = new_armature
            for mod in obj.modifiers:
                if mod.type == 'ARMATURE':
                    mod.object = new_armature
    print("Baking actions...")
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='POSE')
    for track in orig_armature.animation_data.nla_tracks:
        print(track.name)
        for s in track.strips:
            action = s.action
            print(action.name)
            orig_armature.animation_data.action = action
            new_armature.animation_data.action = None
            bpy.context.view_layer.objects.active = new_armature
            firstFrame = int(s.action_frame_start)
            lastFrame = int(s.action_frame_end)
            bpy.ops.nla.bake(frame_start=firstFrame, frame_end=lastFrame, step=5, only_selected=False, visual_keying=True, clear_constraints=False, clean_curves=True, use_current_action=False, bake_types={'POSE'})
            aname = orig_armature.animation_data.action.name
            orig_armature.animation_data.action.name = "bake_" + aname
            new_armature.animation_data.action.name = aname
            track = new_armature.animation_data.nla_tracks.new()
            track.name = aname
            track.strips.new(track.name, int(new_armature.animation_data.action.frame_range[0]), new_armature.animation_data.action)
            track.mute = True
            track.lock = True
    print("Removing constraints...")
    for b in new_armature.pose.bones:
        for c in b.constraints:
            b.constraints.remove(c)
    new_armature.animation_data.action = bpy.data.actions[mapping.default_action]
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='OBJECT')

#        track = new_armature.animation_data.nla_tracks.new()
#        track.name = action.name


def clamp_angle_deg(angle, min_angle_deg, max_angle_deg):
    min_angle = radians(min_angle_deg)
    max_angle = radians(max_angle_deg)
    if angle < min_angle:
        angle = min_angle
    if angle > max_angle:
        angle = max_angle
    return angle
def angle_to_linear(angle, divider):
    if angle < 0.0:
        return angle / divider
    else:
        return 0.0
def angle_to_linear_x(bone, angle):
    skel = bpy.data.objects["skeleton_orig"]
    left_base = "ctrl_base_upperleg.L.001"
    right_base = "ctrl_base_upperleg.R.001"
    base = ""
    if base == "":
        for e in ["_R", ".R"]:
            if bone.name.endswith(e):
                base = right_base
                break
    if base == "":
        for e in ["_L", ".L"]:
            if bone.name.endswith(e):
                base = left_base
                break
    if base == "":
        for e in ["_R.", ".R."]:
            if bone.name.find(e) >= 0:
                base = right_base
                break
    if base == "":
        for e in ["_L.", ".L."]:
            if bone.name.find(e) >= 0:
                base = left_base
                break
    mul = skel.pose.bones[base]["to_linear_x_base"]
    offset = skel.pose.bones[base]["angle_offset"]
#    print("bone: ", bone.name, "base: ", base, "mul: ", mul)
#    print("angle: ", angle, " angle_offset: ", offset, " angle_sum: ", angle + offset)
    print("offset: ", mul * (angle + offset), "bone: ", base, "angle: ", angle)
    return (angle + offset) * mul
def extra_linear(angle, offset):
    ret = 0.0
    offt = offset * angle * 2.0 / -radians(-90)
    if angle * 2.0 < -radians(65):
        if angle * 2.0 > -radians(65):
            ret += offset
        else:
            ret += offt
    return ret

mapping_blend_path = argv[0]
mapping_gltf_path = argv[1]
# Backward compat: support both 4-arg (blend, gltf, armature, outfile) and 5-arg (blend, gltf, objects, armature, outfile)
if len(argv) >= 5:
	mapping_objects = argv[2]
	mapping_armature_name = argv[3]
	mapping_outfile = argv[4]
else:
	mapping_objects = "AUTO"
	mapping_armature_name = argv[2]
	mapping_outfile = argv[3]

#for mapping in [ExportMappingFemale(), ExportMappingMale(), ExportMappingMaleBabyShape(), ExportMappingMaleEdited(), ExportMappingFemaleEdited(), ExportMappingMaleTestShapeEdited(), ExportMappingMaleBaseShapeEdited()]:
class CommandLineMapping:
    blend_path = mapping_blend_path
    gltf_path = mapping_gltf_path
    inner_path = "Object"
    objs = []
    armature_name = mapping_armature_name
    outfile = mapping_outfile
    default_action = 'default'
    auto_discover = False
    def __init__(self):
        self.objs = [mapping_armature_name]
        if len(mapping_objects) > 0 and mapping_objects != "AUTO":
            self.objs += [o.strip() for o in mapping_objects.split(";")]
        else:
            self.auto_discover = True
        self.files = []
        for fobj in self.objs:
            self.files.append({"name": fobj})

for mapping in[CommandLineMapping()]:
    if not os.path.exists(mapping.blend_path):
        print("Skipping mapping: " + mapping.blend_path)
        continue
    print("Processing mapping: from: " + mapping.blend_path + " to: " + mapping.gltf_path)
    print("Initializing...")
    bpy.ops.wm.read_homefile(use_empty=True)
    print("Preparing driver setup...")
    bpy.app.driver_namespace["clamp_angle_deg"] = clamp_angle_deg
    bpy.app.driver_namespace["angle_to_linear"] = angle_to_linear
    bpy.app.driver_namespace["extra_linear"] = extra_linear
    bpy.app.driver_namespace["angle_to_linear_x"] = angle_to_linear_x
    print("Driver setup done...")

    if mapping.auto_discover:
        # Load all objects from the blend file so we can discover meshes by custom props
        with bpy.data.libraries.load(mapping.blend_path) as (data_from, data_to):
            data_to.objects = data_from.objects
        for obj in data_to.objects:
            if obj is not None:
                try:
                    bpy.context.collection.objects.link(obj)
                except RuntimeError:
                    pass  # Already linked
        print("Library load done...")

        discovered = []
        for ob in bpy.data.objects:
            if ob.type == 'MESH' and all(p in ob.keys() for p in ["age", "sex", "slot"]):
                discovered.append(ob.name)
        mapping.objs += discovered
        print(f"Auto-discovered {len(discovered)} body part objects: {discovered}")

        # ---- Separate hair with its own skeleton ----
        hair_own_skel = []
        hair_own_skel_objs = []  # save object references
        for name in list(mapping.objs):
            obj = bpy.data.objects.get(name)
            if obj:
                has_it = obj.get("has_own_armature", False)
                own_arm = obj.get("own_armature_name", "")
                print(f"  Object '{name}': has_own_armature="
                      f"{has_it}, own_armature_name='{own_arm}'")
                if has_it:
                    hair_own_skel.append(name)
                    hair_own_skel_objs.append(obj)
                    mapping.objs.remove(name)
        if hair_own_skel:
            print(f"Hair with own skeleton (exported separately): "
                  f"{hair_own_skel}")
        else:
            print(f"No hair with own skeleton detected")
        # ---------------------------------------------
    else:
        bpy.ops.wm.append(
            filepath=os.path.join(mapping.blend_path, mapping.inner_path),
            directory=os.path.join(mapping.blend_path, mapping.inner_path),
            files=mapping.files)
        print("Append done...")

    prepare_armature(mapping)
    print("Armature done...")

    print("Remove junk...")
    for ob in bpy.data.objects:
        if ob.name.startswith("cs_"):
            bpy.data.objects.remove(ob)
        elif ob.name.startswith("Face") and ob.name != "Face":
            bpy.data.objects.remove(ob)
        # Remove auto-discovered objects that weren't meant for export
        elif mapping.auto_discover and ob.type == 'MESH' and ob.name not in mapping.objs:
            if ob.get("has_own_armature", False):
                continue  # exported separately, don't rename
            if not ob.name.endswith("-noimp"):
                ob.name = ob.name + "-noimp"
    
    # Remove .001 suffixed duplicates that may have leaked from consolidate step
    # These are objects that have the same base name as an object in mapping.objs
    # but with a .001 suffix (e.g., BodyBottom.001 when BodyBottom is in mapping.objs)
    if mapping.auto_discover:
        for ob in list(bpy.data.objects):
            if ob.type == 'MESH' and ob.name.endswith('.001'):
                base_name = ob.name[:-4]
                if base_name in mapping.objs:
                    print(f"Removing duplicate '{ob.name}' (base '{base_name}' already in export list)")
                    bpy.data.objects.remove(ob, do_unlink=True)

    print("Removing original armature and actions...")
    orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
    bpy.data.objects.remove(orig_arm)
    for act in bpy.data.actions:
        if act.name.startswith("bake_"):
            act.name = act.name + "-noimp"
    for act in bpy.data.actions:
        if act.name.startswith("bake_"):
            bpy.data.actions.remove(act)
    for act in bpy.data.actions:
        if act.name.startswith("bake_"):
            bpy.data.actions.remove(act)
    for obj in bpy.data.objects:
        if obj.type == 'MESH':
            # Skip hair-with-own-skeleton objects — they're
            # exported separately and must not be renamed
            if obj.get("has_own_armature", False):
                continue
            if not obj.name in mapping.objs and obj.parent is None:
                if not obj.name.endswith("-noimp"):
                    obj.name = obj.name + "-noimp"
    bpy.ops.wm.save_as_mainfile(filepath=(basepath + "/assets/blender/scripts/" + mapping.outfile))

    # Hide hair-with-own-skeleton objects from the body glTF export.
    # The glTF exporter crashes when sampling animations for hair armatures
    # that are not part of the main rig, so we exclude them here and export
    # hair separately via OGRE later.
    for obj in hair_own_skel_objs:
        obj.hide_viewport = True
        obj.hide_render = True
        arm_name = obj.get("own_armature_name", "")
        hair_arm = bpy.data.objects.get(arm_name)
        if hair_arm:
            hair_arm.hide_viewport = True
            hair_arm.hide_render = True

    os.makedirs(os.path.dirname(mapping.gltf_path), exist_ok=True)
    bpy.ops.export_scene.gltf(filepath=mapping.gltf_path,
            use_selection=False,
            check_existing=False,
#            export_format='GLTF_SEPARATE',
            export_format='GLB',
            export_texture_dir='textures', export_texcoords=True,
            export_animation_mode='NLA_TRACKS',
            export_normals=True,
            export_tangents=True,
            export_materials='EXPORT',
#            export_all_vertex_colors=True,
#            colors_type='SRGB',
#            export_vertex_colors=True,
            export_colors=True,
            use_mesh_edges=False,
            use_mesh_vertices=False,
            export_cameras=False,
            use_visible=True,
            use_renderable=False,
            export_yup=True,
            export_animations=True,
            export_force_sampling=True,
            export_def_bones=False,
            export_current_frame=False,
            export_morph=True,
            export_morph_animation=False,
            export_morph_normal=True,
            export_morph_tangent=True,
            export_lights=False,
            export_skins=True)
    print("exported to: " + mapping.gltf_path)

    # Unhide hair objects for OGRE export
    for obj in hair_own_skel_objs:
        obj.hide_viewport = False
        obj.hide_render = False
        arm_name = obj.get("own_armature_name", "")
        hair_arm = bpy.data.objects.get(arm_name)
        if hair_arm:
            hair_arm.hide_viewport = False
            hair_arm.hide_render = False
    obj_names = mapping.objs
    prefix = mapping.armature_name + "_"

    for name in obj_names:
        obj = bpy.data.objects.get(name)

        if obj and obj.type == 'MESH':
            # 1. Rename Mesh Data to match object name (not mesh data name)
            # This prevents .001 suffixed mesh data names (e.g., BodyBottom.001)
            # from becoming male_BodyBottom.001 and clashing with male_BodyBottom.
            new_mesh_name = prefix + name
            if obj.data.name != new_mesh_name:
                # Check if another mesh data already has this name
                if new_mesh_name in bpy.data.meshes and bpy.data.meshes[new_mesh_name] != obj.data:
                    old = bpy.data.meshes[new_mesh_name]
                    old.name = new_mesh_name + "_old"
                    print(f"Renamed conflicting mesh data '{old.name}' for object '{name}'")
                obj.data.name = new_mesh_name
                print(f"Renamed mesh data from '{obj.data.name}' to '{new_mesh_name}' for object '{name}'")

            # 2. Iterate through all Material Slots on the object
            for slot in obj.material_slots:
                if slot.material:
                    mat = slot.material
                    # 3. Normalize material name: strip Blender auto-suffixes
                    #    (.001,.002 from name collisions, .### from dupes)
                    #    before applying the armature prefix.  This prevents
                    #    duplicate .material files with unpredictable names
                    #    (e.g. male_male-clothes-ed.001 vs male_male-clothes-ed)
                    #    that cause submeshes to reference missing materials
                    #    at runtime.
                    import re
                    clean = re.sub(r'\.\d{3}$', '', mat.name)
                    if clean != mat.name:
                        old = mat.name
                        mat.name = clean
                        # If Blender added .001 again because the clean
                        # name is already taken, keep the suffixed name.
                        print(f"Normalized material '{old}' -> '{mat.name}'"
                              f" on object '{name}'")
                    # 4. Check if material already has the prefix
                    if not mat.name.startswith(prefix):
                        mat.name = prefix + mat.name
                        print(f"Renamed material '{mat.name}'"
                              f" on object '{name}'")
            # 3. Export custom properties to json
            save_data = {}
            for key in obj.keys():
                if key in ["age", "sex", "slot"]:
                    save_data[key] = obj[key]
                if key.startswith("body_"):
                    save_data[key.replace("body_", "", 1)] = obj[key]

            # Export aggregated clothing metadata
            if "layer" in obj:
                save_data["layer"] = int(obj["layer"])
            else:
                save_data["layer"] = 0

            tags = []
            if "garments" in obj and obj["garments"]:
                garments = [g.strip() for g in str(obj["garments"]).split(";") if g.strip()]
                save_data["garments"] = garments
                tags.extend(garments)
            else:
                save_data["garments"] = []

            if "clothing_tags" in obj and obj["clothing_tags"]:
                clothing_tags = [t.strip() for t in str(obj["clothing_tags"]).split(";") if t.strip()]
                tags.extend(clothing_tags)

            # Deduplicate and sort
            seen = set()
            unique_tags = []
            for t in tags:
                t_lower = t.lower()
                if t_lower not in seen:
                    seen.add(t_lower)
                    unique_tags.append(t)
            save_data["tags"] = unique_tags

            # Export shape keys if present
            # IMPORTANT: Skip "Basis" to match OGRE's pose indexing.
            # OGRE's blender2ogre exporter skips the Basis shape key (index 0),
            # so pose index 0 in OGRE = first non-Basis shape key.
            shape_keys = []
            if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
                for sk in obj.data.shape_keys.key_blocks:
                    if sk.name != 'Basis':
                        shape_keys.append(sk.name)
            save_data["shape_keys"] = shape_keys

            save_data["mesh"] = obj.data.name + ".mesh"
            json_dir = os.path.dirname(mapping.gltf_path)
            save_file = json_dir + "/body_part_" + obj.data.name + ".json"
            json_filepath = os.path.join(json_dir, save_file)
            with open(json_filepath, 'w') as f:
                json.dump(save_data, f, indent=2)

    # Triangulate body part meshes before OGRE export to prevent the exporter's
    # triangulation from producing mismatched split normals at seams.
    # BodyTop and BodyBottom have different face topologies, so the exporter's
    # bmesh triangulation creates slightly different corner normals for matching
    # vertices. Pre-triangulating ensures the exporter sees already-triangulated
    # meshes and preserves our custom normals.
    import bmesh
    body_part_objs = []
    for ob in bpy.data.objects:
        if ob.type == 'MESH' and all(p in ob for p in ["age", "sex", "slot"]):
            body_part_objs.append(ob)

    print(f"\nTriangulating {len(body_part_objs)} body part meshes for OGRE export...")
    for obj in body_part_objs:
        mesh = obj.data
        bm = bmesh.new()
        bm.from_mesh(mesh)
        bmesh.ops.triangulate(bm, faces=bm.faces)
        bm.to_mesh(mesh)
        bm.free()
        mesh.calc_normals()
        print(f"  Triangulated '{obj.name}': {len(mesh.polygons)} polygons")

    # Re-run normal fix on triangulated meshes to ensure matching vertices
    # have identical custom split normals after triangulation.
    chars_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "characters")
    if chars_dir not in sys.path:
        sys.path.insert(0, chars_dir)
    from transfer_shape_keys import fix_normals_across_objects
    fix_normals_across_objects()

    armobj = bpy.data.objects.get(mapping.armature_name)
    armobj.data.name = armobj.name

    # ---- Fix: sync Image filepath to Image name ----
    # Blender silently reuses existing Images when appending
    # from libraries, so the Image's filepath may still point
    # to the old texture even after the user renamed it.
    # blender2ogre writes the filepath to .material files,
    # so we must update filepath to match the Image name.
    import re as _re
    seen_imgs = set()
    for name in obj_names:
        obj = bpy.data.objects.get(name)
        if obj and obj.type == 'MESH':
            for slot in obj.material_slots:
                if slot.material and slot.material.node_tree:
                    for node in slot.material.node_tree.nodes:
                        if node.type == 'TEX_IMAGE' and node.image:
                            img = node.image
                            if img.name not in seen_imgs:
                                seen_imgs.add(img.name)
                                # Strip Blender auto-suffix (.NNN)
                                clean = _re.sub(
                                    r'\.\d{3}$', '', img.name)
                                # Extract just the filename from
                                # the current filepath
                                old_name = os.path.basename(
                                    img.filepath)
                                if old_name != clean and clean:
                                    # Rebuild filepath with new
                                    # filename
                                    dirpart = os.path.dirname(
                                        img.filepath)
                                    new_path = os.path.join(
                                        dirpart, clean)
                                    print(f"  Updating Image "
                                          f"'{img.name}': "
                                          f"filepath "
                                          f"'{old_name}' -> "
                                          f"'{clean}'")
                                    img.filepath = new_path
    # ---------------------------------------------------

    bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')

    ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))

    # Fix alpha_rejection values in ALL generated .material files.
    # blender2ogre 0.9.0 writes float values (e.g. "127.5") for
    # alpha_rejection thresholds, but OGRE 14 expects an integer
    # in the range 0-255.  Float values are truncated to integer
    # during material compilation, causing 127.5 -> 127 when 128
    # was intended.  This makes submeshes with alpha < 128 in the
    # texture atlas completely invisible.
    import glob as _glob
    _mat_files = _glob.glob(os.path.join(ogre_export_dir, "*.material"))
    for _mf in _mat_files:
        with open(_mf, 'r') as _f:
            _content = _f.read()
        if 'alpha_rejection' in _content:
            import re as _re
            _new = _re.sub(
                r'alpha_rejection (\S+) (\d+\.?\d*)',
                lambda m: 'alpha_rejection ' + m.group(1) +
                          ' ' + str(int(float(m.group(2)))),
                _content)
            if _new != _content:
                with open(_mf, 'w') as _f:
                    _f.write(_new)
                print(f"  Fixed alpha_rejection in {_mf}")

    # Post-process exported OGRE meshes to fix normal/tangent seams between
    # body parts. Even with identical custom normals in Blender, the OGRE
    # exporter's calc_tangents() produces slightly different tangents for
    # matching vertices because BodyTop and BodyBottom have different face
    # topologies. This causes a visible lighting seam with normal mapping,
    # especially when shape keys (like "fat") are applied.
    fix_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fix_ogre_mesh_seams.py")
    if os.path.exists(fix_script):
        print(f"\nPost-processing OGRE meshes in {ogre_export_dir}...")
        import subprocess
        import shutil
        # Use system python3 if available, since sys.executable in Blender
        # may point to the Blender binary itself, not a Python interpreter.
        python_exe = shutil.which('python3') or shutil.which('python') or sys.executable
        result = subprocess.run(
            [python_exe, fix_script, ogre_export_dir],
            capture_output=True, text=True
        )
        print(result.stdout)
        if result.returncode != 0:
            print(f"WARNING: Mesh seam fix failed: {result.stderr}")
    else:
        print(f"WARNING: fix_ogre_mesh_seams.py not found at {fix_script}")

    # ---- Export hair with own skeleton ----
    print(f"\n=== HAIR DEBUG: auto_discover={mapping.auto_discover} "
          f"hair_own_skel={hair_own_skel}")
    if mapping.auto_discover and hair_own_skel:
        # Collect hair meshes grouped by their armature name
        hair_by_arm = {}
        for obj in hair_own_skel_objs:
            arm_name = obj.get("own_armature_name", "")
            print(f"  HAIR DEBUG: obj '{obj.name}' "
                  f"own_armature_name='{arm_name}' "
                  f"has_own_armature={obj.get('has_own_armature', False)}")
            if arm_name not in hair_by_arm:
                hair_by_arm[arm_name] = []
            hair_by_arm[arm_name].append(obj)
        print(f"  HAIR DEBUG: hair_by_arm has {len(hair_by_arm)} groups")

        body_arm_name = mapping.armature_name  # save "male"/"female"
        body_prefix = body_arm_name + "_"

        # Debug: list all armatures in the scene
        all_arms = [o.name for o in bpy.data.objects
                    if o.type == 'ARMATURE']
        print(f"  HAIR DEBUG: armatures in scene: {all_arms}")

        for arm_name, hair_objs in hair_by_arm.items():
            hair_arm = bpy.data.objects.get(arm_name)
            print(f"  HAIR DEBUG: arm_name='{arm_name}' "
                  f"hair_arm_found={hair_arm is not None}")
            if not hair_arm or hair_arm.type != 'ARMATURE':
                print(f"  WARNING: Armature '{arm_name}' not found "
                      f"for hair, skipping")
                continue

            hair_obj_names = [o.name for o in hair_objs]
            print(f"  Exporting hair with skeleton '{arm_name}': "
                  f"{hair_obj_names}")

            # Sync armature data name
            hair_arm.data.name = hair_arm.name
            print(f"  Armature data name: '{hair_arm.data.name}'")

            # Write body_part JSON for each hair mesh
            json_dir = os.path.dirname(mapping.gltf_path)
            for obj in hair_objs:
                new_mesh_name = body_prefix + obj.name
                obj.data.name = new_mesh_name
                save_data = {
                    "age": obj.get("age", ""),
                    "sex": obj.get("sex", ""),
                    "slot": obj.get("slot", ""),
                    "mesh": obj.data.name + ".mesh",
                    "layer": int(obj.get("layer", 0)),
                    "garments": [],
                    "tags": [],
                    "shape_keys": [],
                    "own_skeleton": True,
                    "attach_to_bone": "mixamorig:Head"
                }
                garments = obj.get("garments", "")
                if garments:
                    save_data["garments"] = [
                        g.strip()
                        for g in str(garments).split(";")
                        if g.strip()]
                clothing_tags = obj.get("clothing_tags", "")
                if clothing_tags:
                    save_data["tags"] = [
                        t.strip()
                        for t in str(clothing_tags).split(";")
                        if t.strip()]
                save_file = (json_dir + "/body_part_" +
                             obj.data.name + ".json")
                with open(save_file, 'w') as f:
                    json.dump(save_data, f, indent=2)
                print(f"  Wrote {save_file}")

            # OGRE export for hair with its own skeleton

            # OGRE export for hair with its own skeleton
            hair_scene = mapping.gltf_path.replace(
                ".glb", "_hair_" + arm_name + ".scene")
            bpy.ops.ogre.export(
                filepath=hair_scene,
                EX_SELECTED_ONLY=False,
                EX_SHARED_ARMATURE=True,
                EX_LOD_GENERATION='0',
                EX_LOD_DISTANCE=20,
                EX_LOD_LEVELS=4,
                EX_GENERATE_TANGENTS='4')
            print(f"  Exported hair scene: {hair_scene}")

            # Fix alpha_rejection in hair .material files
            _mat_files = _glob.glob(
                os.path.join(ogre_export_dir, "*.material"))
            for _mf in _mat_files:
                with open(_mf, 'r') as _f:
                    _content = _f.read()
                if 'alpha_rejection' in _content:
                    _new = _re.sub(
                        r'alpha_rejection (\S+) (\d+\.?\d*)',
                        lambda m: 'alpha_rejection ' +
                                  m.group(1) + ' ' +
                                  str(int(float(m.group(2)))),
                        _content)
                    if _new != _content:
                        with open(_mf, 'w') as _f:
                            _f.write(_new)
    # -----------------------------------------

bpy.ops.wm.read_homefile(use_empty=True)
time.sleep(2)
bpy.ops.wm.quit_blender()
