#!/usr/bin/env python3
"""
Garment System UI Panel for Blender

Run this script in Blender's Scripting editor (or via blender -b -P garment_system_ui.py)
to add a side-panel for editing garment-system custom properties on mesh objects.

Properties managed:
  Body Part  -> age, sex, slot, layer, garments, clothing_tags
  Clothing   -> ref_layer, ref_part, garment_id, ref_sex, ref_age, ref_clothing, tags
"""

import bpy

# ---------------------------------------------------------------------------
# Property helpers
# ---------------------------------------------------------------------------

SLOT_ITEMS = [
    ("face", "Face", ""),
    ("bottom", "Bottom", ""),
    ("top", "Top", ""),
    ("feet", "Feet", ""),
    ("hair", "Hair", ""),
]

SEX_ITEMS = [
    ("male", "Male", ""),
    ("female", "Female", ""),
]

AGE_ITEMS = [
    ("adult", "Adult", ""),
    ("child", "Child", ""),
    ("teen", "Teen", ""),
    ("baby", "Baby", ""),
]


def _ensure_prop(obj, key, default):
    if key not in obj:
        obj[key] = default
    return obj[key]


def _get_prop_str(obj, key, default=""):
    if key not in obj:
        return default
    return str(obj[key])


def _get_prop_int(obj, key, default=0):
    if key not in obj:
        return default
    try:
        return int(obj[key])
    except (ValueError, TypeError):
        return default


def _set_prop_str(obj, key, value):
    if value:
        obj[key] = value
    elif key in obj:
        del obj[key]


def _set_prop_int(obj, key, value):
    obj[key] = int(value)


def _del_prop(obj, key):
    if key in obj:
        del obj[key]


# ---------------------------------------------------------------------------
# Operators
# ---------------------------------------------------------------------------

class GARMENT_OT_apply_body_part(bpy.types.Operator):
    bl_idname = "garment.apply_body_part"
    bl_label = "Apply Body Part"
    bl_description = "Write body-part custom properties to the active object"
    bl_options = {"REGISTER", "UNDO"}

    age: bpy.props.EnumProperty(name="Age", items=AGE_ITEMS)
    sex: bpy.props.EnumProperty(name="Sex", items=SEX_ITEMS)
    slot: bpy.props.EnumProperty(name="Slot", items=SLOT_ITEMS)
    layer: bpy.props.IntProperty(name="Layer", min=0, max=2, default=0)
    garments: bpy.props.StringProperty(name="Garments", description="Semicolon-separated garment names")
    clothing_tags: bpy.props.StringProperty(name="Clothing Tags", description="Semicolon-separated tags")

    def execute(self, context):
        obj = context.active_object
        if not obj or obj.type != "MESH":
            self.report({"WARNING"}, "Active object is not a mesh")
            return {"CANCELLED"}

        _set_prop_str(obj, "age", self.age)
        _set_prop_str(obj, "sex", self.sex)
        _set_prop_str(obj, "slot", self.slot)
        _set_prop_int(obj, "layer", self.layer)
        _set_prop_str(obj, "garments", self.garments)
        _set_prop_str(obj, "clothing_tags", self.clothing_tags)

        # Remove clothing-only props so they don't confuse the pipeline
        for k in ("ref_layer", "ref_part", "garment_id", "ref_sex",
                  "ref_age", "ref_clothing", "tags"):
            _del_prop(obj, k)

        self.report({"INFO"}, f"Body-part props applied to {obj.name}")
        return {"FINISHED"}

    def invoke(self, context, event):
        obj = context.active_object
        if obj:
            self.age = _get_prop_str(obj, "age", "adult")
            self.sex = _get_prop_str(obj, "sex", "male")
            self.slot = _get_prop_str(obj, "slot", "bottom")
            self.layer = _get_prop_int(obj, "layer", 0)
            self.garments = _get_prop_str(obj, "garments", "")
            self.clothing_tags = _get_prop_str(obj, "clothing_tags", "")
        return context.window_manager.invoke_props_dialog(self)


class GARMENT_OT_apply_clothing(bpy.types.Operator):
    bl_idname = "garment.apply_clothing"
    bl_label = "Apply Clothing"
    bl_description = "Write clothing custom properties to the active object"
    bl_options = {"REGISTER", "UNDO"}

    ref_layer: bpy.props.IntProperty(name="Ref Layer", min=1, max=2, default=1,
                                      description="1 = lingerie, 2 = clothing")
    ref_part: bpy.props.StringProperty(name="Ref Part",
                                        description="Target body-part object name, e.g. BodyBottom")
    garment_id: bpy.props.StringProperty(name="Garment ID",
                                          description="Garment name used for labels, e.g. panties7")
    ref_sex: bpy.props.EnumProperty(name="Ref Sex", items=SEX_ITEMS)
    ref_age: bpy.props.EnumProperty(name="Ref Age", items=AGE_ITEMS)
    ref_clothing: bpy.props.StringProperty(name="Ref Clothing",
                                            description="Mesh name used for weight transfer, e.g. BodyBottom")
    tags: bpy.props.StringProperty(name="Tags", description="Semicolon-separated tags")

    def execute(self, context):
        obj = context.active_object
        if not obj or obj.type != "MESH":
            self.report({"WARNING"}, "Active object is not a mesh")
            return {"CANCELLED"}

        _set_prop_int(obj, "ref_layer", self.ref_layer)
        _set_prop_str(obj, "ref_part", self.ref_part)
        _set_prop_str(obj, "garment_id", self.garment_id)
        _set_prop_str(obj, "ref_sex", self.ref_sex)
        _set_prop_str(obj, "ref_age", self.ref_age)
        _set_prop_str(obj, "ref_clothing", self.ref_clothing)
        _set_prop_str(obj, "tags", self.tags)

        # Remove body-part-only props so they don't confuse the pipeline
        for k in ("age", "sex", "slot", "layer", "garments", "clothing_tags"):
            _del_prop(obj, k)

        self.report({"INFO"}, f"Clothing props applied to {obj.name}")
        return {"FINISHED"}

    def invoke(self, context, event):
        obj = context.active_object
        if obj:
            self.ref_layer = _get_prop_int(obj, "ref_layer", 1)
            self.ref_part = _get_prop_str(obj, "ref_part", "")
            self.garment_id = _get_prop_str(obj, "garment_id", "")
            self.ref_sex = _get_prop_str(obj, "ref_sex", "male")
            self.ref_age = _get_prop_str(obj, "ref_age", "adult")
            self.ref_clothing = _get_prop_str(obj, "ref_clothing", "")
            self.tags = _get_prop_str(obj, "tags", "")
        return context.window_manager.invoke_props_dialog(self)


class GARMENT_OT_auto_detect(bpy.types.Operator):
    bl_idname = "garment.auto_detect"
    bl_label = "Auto-detect from Name"
    bl_description = "Guess sex/slot from object name"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        obj = context.active_object
        if not obj or obj.type != "MESH":
            self.report({"WARNING"}, "Active object is not a mesh")
            return {"CANCELLED"}

        name = obj.name.lower()
        changed = []

        # Guess sex
        if "female" in name:
            obj["sex"] = "female"
            changed.append("sex=female")
        elif "male" in name:
            obj["sex"] = "male"
            changed.append("sex=male")

        # Guess slot
        slot_map = {
            "face": "face",
            "bottom": "bottom",
            "top": "top",
            "feet": "feet",
            "hair": "hair",
        }
        for key, slot in slot_map.items():
            if key in name:
                obj["slot"] = slot
                changed.append(f"slot={slot}")
                break

        # Guess layer from name hints
        if any(x in name for x in ("panties", "lingerie", "underwear")):
            obj["layer"] = 1
            changed.append("layer=1")
        elif any(x in name for x in ("pants", "skirt", "robe", "shirt", "dress", "cloth")):
            obj["layer"] = 2
            changed.append("layer=2")
        else:
            obj["layer"] = 0
            changed.append("layer=0")

        # Set garment_id from object name for clothing
        if "ref_layer" in obj or "ref_part" in obj:
            if "garment_id" not in obj or not obj["garment_id"]:
                obj["garment_id"] = obj.name
                changed.append(f"garment_id={obj.name}")

        self.report({"INFO"}, "Auto-detect: " + ", ".join(changed) if changed else "nothing changed")
        return {"FINISHED"}


class GARMENT_OT_clear_props(bpy.types.Operator):
    bl_idname = "garment.clear_props"
    bl_label = "Clear All Garment Props"
    bl_description = "Remove all garment-system custom properties from the active object"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        obj = context.active_object
        if not obj:
            return {"CANCELLED"}
        keys = list(obj.keys())
        removed = []
        for k in keys:
            if k in ("age", "sex", "slot", "layer", "garments", "clothing_tags",
                     "ref_layer", "ref_part", "garment_id", "ref_sex", "ref_age",
                     "ref_clothing", "tags"):
                del obj[k]
                removed.append(k)
        self.report({"INFO"}, f"Removed {len(removed)} props" if removed else "No garment props found")
        return {"FINISHED"}


class GARMENT_OT_sync_tags(bpy.types.Operator):
    bl_idname = "garment.sync_tags"
    bl_label = "Sync Tags from Garments"
    bl_description = "Copy clothing_tags from garments field"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        obj = context.active_object
        if not obj:
            return {"CANCELLED"}
        garments = _get_prop_str(obj, "garments", "")
        obj["clothing_tags"] = garments
        self.report({"INFO"}, f"Tags set to: {garments}")
        return {"FINISHED"}


class GARMENT_OT_check_scene(bpy.types.Operator):
    bl_idname = "garment.check_scene"
    bl_label = "Check Scene"
    bl_description = "Validate all garment-system objects for pipeline readiness"
    bl_options = {"REGISTER"}

    def execute(self, context):
        errors = []
        warnings = []
        body_count = 0
        cloth_count = 0
        ok_count = 0

        required_body_props = {"age", "sex", "slot"}
        required_clothing_props = {"ref_layer", "ref_part", "garment_id"}

        for obj in bpy.data.objects:
            if obj.type != "MESH":
                continue

            has_body_props = all(p in obj for p in required_body_props)
            has_clothing_props = all(p in obj for p in required_clothing_props)

            if has_body_props and has_clothing_props:
                errors.append(f"'{obj.name}': has BOTH body-part and clothing properties (mixed)")
                continue

            if has_body_props:
                body_count += 1
                # Check body part values are valid
                age = str(obj.get("age", ""))
                sex = str(obj.get("sex", ""))
                slot = str(obj.get("slot", ""))
                valid_ages = {"adult", "child", "teen", "baby"}
                valid_sexes = {"male", "female"}
                valid_slots = {"face", "bottom", "top", "feet", "hair"}
                if age not in valid_ages:
                    errors.append(f"'{obj.name}': invalid age='{age}' (valid: {', '.join(sorted(valid_ages))})")
                if sex not in valid_sexes:
                    errors.append(f"'{obj.name}': invalid sex='{sex}' (valid: {', '.join(sorted(valid_sexes))})")
                if slot not in valid_slots:
                    errors.append(f"'{obj.name}': invalid slot='{slot}' (valid: {', '.join(sorted(valid_slots))})")
                ok_count += 1

            elif has_clothing_props:
                cloth_count += 1
                # Check clothing values are valid
                ref_sex = str(obj.get("ref_sex", ""))
                ref_age = str(obj.get("ref_age", ""))
                valid_ages = {"adult", "child", "teen", "baby"}
                valid_sexes = {"male", "female"}
                if ref_sex and ref_sex not in valid_sexes:
                    errors.append(f"'{obj.name}': invalid ref_sex='{ref_sex}' (valid: {', '.join(sorted(valid_sexes))})")
                if ref_age and ref_age not in valid_ages:
                    errors.append(f"'{obj.name}': invalid ref_age='{ref_age}' (valid: {', '.join(sorted(valid_ages))})")
                # Check ref_part points to an existing body object
                ref_part = str(obj.get("ref_part", ""))
                if ref_part and ref_part not in bpy.data.objects:
                    warnings.append(f"'{obj.name}': ref_part='{ref_part}' not found in scene")
                ok_count += 1

            else:
                # Mesh object with no garment props - check if it should have them
                if obj.name.startswith("Body") or any(x in obj.name.lower() for x in ("shoes", "pants", "shirt", "skirt", "dress", "hat", "hair", "top", "bottom")):
                    warnings.append(f"'{obj.name}': mesh object with no garment properties (may need age/sex/slot or ref_layer/ref_part/garment_id)")

        # Report results
        self.report({"INFO"}, f"Check complete: {body_count} body parts, {cloth_count} clothing, {len(errors)} errors, {len(warnings)} warnings")

        # Print detailed results to console
        print("\n" + "=" * 60)
        print("GARMENT SYSTEM SCENE CHECK")
        print("=" * 60)
        print(f"Body parts: {body_count}")
        print(f"Clothing:    {cloth_count}")
        print(f"OK objects:  {ok_count}")
        print(f"Errors:      {len(errors)}")
        print(f"Warnings:    {len(warnings)}")
        print("-" * 60)

        if errors:
            print("\nERRORS (must fix before pipeline run):")
            for e in errors:
                print(f"  [ERROR] {e}")

        if warnings:
            print("\nWARNINGS (review recommended):")
            for w in warnings:
                print(f"  [WARN]  {w}")

        if not errors and not warnings:
            print("\n  ✓ Scene looks good! Pipeline should run successfully.")
        print("=" * 60)

        return {"FINISHED"}


# ---------------------------------------------------------------------------
# Panel
# ---------------------------------------------------------------------------

class GARMENT_PT_panel(bpy.types.Panel):
    bl_label = "Garment System"
    bl_idname = "GARMENT_PT_panel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Garment"

    def draw(self, context):
        layout = self.layout
        obj = context.active_object

        if not obj:
            layout.label(text="No active object")
            return

        layout.label(text=f"Object: {obj.name}", icon="OBJECT_DATA")

        # Detect current type
        is_body = any(k in obj for k in ("age", "sex", "slot"))
        is_cloth = any(k in obj for k in ("ref_layer", "ref_part", "garment_id"))

        if is_body and is_cloth:
            layout.alert = True
            layout.label(text="WARNING: mixed props", icon="ERROR")
            layout.alert = False
        elif is_body:
            layout.label(text="Type: Body Part", icon="MESH_DATA")
        elif is_cloth:
            layout.label(text="Type: Clothing", icon="MOD_CLOTH")
        else:
            layout.label(text="Type: (unset)", icon="QUESTION")

        layout.separator()

        # Quick action buttons
        row = layout.row(align=True)
        row.operator("garment.apply_body_part", icon="MESH_DATA")
        row = layout.row(align=True)
        row.operator("garment.apply_clothing", icon="MOD_CLOTH")

        layout.separator()
        row = layout.row(align=True)
        row.operator("garment.auto_detect", icon="VIEWZOOM")
        row.operator("garment.clear_props", icon="X")

        layout.separator()

        # Show current body-part props
        if is_body or not is_cloth:
            box = layout.box()
            box.label(text="Body Part Props", icon="MESH_DATA")
            col = box.column(align=True)
            col.label(text=f"age: {_get_prop_str(obj, 'age', '—')}")
            col.label(text=f"sex: {_get_prop_str(obj, 'sex', '—')}")
            col.label(text=f"slot: {_get_prop_str(obj, 'slot', '—')}")
            col.label(text=f"layer: {_get_prop_int(obj, 'layer', 0)}")
            col.label(text=f"garments: {_get_prop_str(obj, 'garments', '—')}")
            col.label(text=f"clothing_tags: {_get_prop_str(obj, 'clothing_tags', '—')}")
            if _get_prop_str(obj, "garments", ""):
                col.operator("garment.sync_tags", icon="FILE_REFRESH")

        # Show current clothing props
        if is_cloth:
            box = layout.box()
            box.label(text="Clothing Props", icon="MOD_CLOTH")
            col = box.column(align=True)
            col.label(text=f"ref_layer: {_get_prop_int(obj, 'ref_layer', 1)}")
            col.label(text=f"ref_part: {_get_prop_str(obj, 'ref_part', '—')}")
            col.label(text=f"garment_id: {_get_prop_str(obj, 'garment_id', '—')}")
            col.label(text=f"ref_sex: {_get_prop_str(obj, 'ref_sex', '—')}")
            col.label(text=f"ref_age: {_get_prop_str(obj, 'ref_age', '—')}")
            col.label(text=f"ref_clothing: {_get_prop_str(obj, 'ref_clothing', '—')}")
            col.label(text=f"tags: {_get_prop_str(obj, 'tags', '—')}")

        layout.separator()

        # Check scene button
        layout.separator()
        row = layout.row(align=True)
        row.scale_y = 1.5
        row.operator("garment.check_scene", icon="CHECKBOX_HLT")

        # Scene overview
        box = layout.box()
        box.label(text="Scene Overview", icon="SCENE_DATA")
        body_count = 0
        cloth_count = 0
        for o in bpy.data.objects:
            if o.type != "MESH":
                continue
            if any(k in o for k in ("age", "sex", "slot")):
                body_count += 1
            elif any(k in o for k in ("ref_layer", "ref_part", "garment_id")):
                cloth_count += 1
        col = box.column(align=True)
        col.label(text=f"Body parts: {body_count}")
        col.label(text=f"Clothing: {cloth_count}")


# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------

classes = [
    GARMENT_OT_apply_body_part,
    GARMENT_OT_apply_clothing,
    GARMENT_OT_auto_detect,
    GARMENT_OT_clear_props,
    GARMENT_OT_sync_tags,
    GARMENT_OT_check_scene,
    GARMENT_PT_panel,
]


def register():
    for cls in classes:
        bpy.utils.register_class(cls)


def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()
