import traceback
import bpy
import numpy as np
from typing import List, Optional, Dict, Set, Tuple, Any
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
from ...core.logging_setup import logger
from ...core.translations import t
import traceback
from ...core.common import (
    get_all_meshes,
    fix_zero_length_bones,
    remove_unused_vertex_groups,
    clear_unused_data_blocks,
    join_mesh_objects,
    remove_unused_shapekeys,
    identify_bones,
    store_breaking_settings_armature,
    restore_breaking_settings_armature,
)
from ...core.dictionaries import simplify_bonename

class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
    """Operator for merging two armatures together with their associated meshes"""
    bl_idname: str = 'avatar_toolkit.merge_armatures'
    bl_label: str = t('MergeArmature.label')
    bl_description: str = t('MergeArmature.desc')
    bl_options: Set[str] = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context: Context) -> bool:
        return len(get_all_meshes(context)) > 1

    def execute(self, context: Context) -> Set[str]:
        try:
            wm = context.window_manager
            wm.progress_begin(0, 100)

            # Get both armatures
            base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
            merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
            base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
            merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
            
            if not base_armature or not merge_armature:
                logger.error(f"Armature not found: {merge_armature_name}")
                self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
                return {'CANCELLED'}
            #Store current armature settings that can mess us up.
            data_breaking_base = store_breaking_settings_armature(base_armature)
            data_breaking_merge = store_breaking_settings_armature(merge_armature)
            
            # Store the merge armature name before it gets removed during join
            merge_armature_name_stored = merge_armature.name

            # Remove Rigid Bodies and Joints
            delete_rigidbodies_and_joints(base_armature)
            delete_rigidbodies_and_joints(merge_armature)
            wm.progress_update(40)

            # Check parents and transformations
            if not validate_parents_and_transforms(merge_armature, base_armature, context):
                wm.progress_end()
                return {'CANCELLED'}
            wm.progress_update(80)

            # Get settings from scene properties
            join_meshes: bool = context.scene.avatar_toolkit.join_meshes

            # Merge armatures
            merge_armatures(
                base_armature_name,
                merge_armature_name,
                mesh_only=False,
                join_meshes=join_meshes,
                operator=self
            )
            wm.progress_update(90)

            wm.progress_update(100)
            wm.progress_end()

            restore_breaking_settings_armature(base_armature, data_breaking_base)
            
            if merge_armature_name_stored in bpy.data.objects:
                merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
                restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge)
            
            self.report({'INFO'}, t('MergeArmature.success'))
            
            return {'FINISHED'}

        except Exception as e:
            logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}")
            self.report({'ERROR'}, traceback.format_exc())
            return {'CANCELLED'}

def delete_rigidbodies_and_joints(armature: Object) -> None:
    """Delete rigid bodies and joints associated with an armature"""
    to_delete: List[Object] = []
    parent: Object = armature
    while parent.parent:
        parent = parent.parent
        
    for child in parent.children:
        if 'rigidbodies' in child.name.lower() or 'joints' in child.name.lower():
            to_delete.append(child)
        for grandchild in child.children:
            if 'rigidbodies' in grandchild.name.lower() or 'joints' in grandchild.name.lower():
                to_delete.append(grandchild)
    
    for obj in to_delete:
        bpy.data.objects.remove(obj, do_unlink=True)

def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool:
    """Validate parent relationships and transformations of armatures"""
    merge_parent: Optional[Object] = merge_armature.parent
    base_parent: Optional[Object] = base_armature.parent
    
    if merge_parent or base_parent:
        for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
            if parent:
                if not is_transform_clean(parent):
                    logger.error("Parent transforms are not clean")
                    return False
                bpy.data.objects.remove(parent, do_unlink=True)
    return True

def is_transform_clean(obj: Object) -> bool:
    """Check if object transforms are at default values"""
    for i in range(3):
        if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0:
            return False
    return True

def prepare_mesh_vertex_groups(mesh: Object) -> None:
    """Initialize mesh vertex groups for merging process"""
    if mesh.vertex_groups:
        for vg in mesh.vertex_groups:
            mesh.vertex_groups.remove(vg)

    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    vg: VertexGroup = mesh.vertex_groups.new(name=mesh.name)
    bpy.ops.object.vertex_group_assign()
    bpy.ops.object.mode_set(mode='OBJECT')

def merge_armatures(
    base_armature_name: str,
    merge_armature_name: str,
    mesh_only: bool,
    join_meshes: bool = False,
    operator: Optional[Operator] = None
) -> None:
    """Main function to merge two armatures with their associated meshes and data"""
    logger.info(f"Merging armatures: {merge_armature_name} into {base_armature_name}")
    tolerance: float = 0.00008726647  # around 0.005 degrees

    base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
    merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)

    if not base_armature or not merge_armature:
        logger.error(f"Armature not found: {merge_armature_name}")
        if operator:
            operator.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
        return

    # Store meshes that need to be reparented
    meshes_to_reparent = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == merge_armature]

    base_armature.hide_set(False)
    merge_armature.hide_set(False)
    
    # Check transforms early
    if not validate_merge_armature_transforms(base_armature, merge_armature, None, tolerance):
        if not bpy.context.scene.avatar_toolkit.apply_transforms:
            logger.error("Transforms not aligned - user notification sent")
            if operator:
                operator.report({'ERROR'}, t('MergeArmature.error.transforms_not_aligned'))
            return

    # Apply transforms if enabled
    if bpy.context.scene.avatar_toolkit.apply_transforms:
        for obj in [base_armature, merge_armature]:
            obj.select_set(True)
            bpy.context.view_layer.objects.active = obj
            bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
            obj.select_set(False)

    # Validate and fix armatures
    fix_zero_length_bones(base_armature)
    fix_zero_length_bones(merge_armature)

    

    # Store original parent relationships
    original_parents: Dict[str, Optional[str]] = {}
    merge_armature_data: bpy.types.Armature = merge_armature.data
    for bone in merge_armature_data.bones:
        original_parents[bone.name] = bone.parent.name if bone.parent else None

    # Switch to edit mode on merge armature and rename bones
    bpy.context.view_layer.objects.active = merge_armature
    bpy.ops.object.mode_set(mode='EDIT')
    
    # Identify our bones to what their standard name is like "hips" for source and target armature bones.
    identifed_base_bone_names: Dict[str,str] = identify_bones(base_armature.data)
    identified_bone_names_source: Dict[str,str] = identify_bones(merge_armature_data)
    
    for standard,bone_name in identified_bone_names_source.items():
        if standard in identifed_base_bone_names: #if the bone we are at on our merge armature has a standard name translation for the target armature
            merge_armature_data.edit_bones[bone_name].name = identifed_base_bone_names[standard] #change it's name to the one on the target merge to armature's coorisponding standard bone
            bone_name = identifed_base_bone_names[standard]
            #adjust original parents list to point to the new name.
            for child_bone in merge_armature_data.edit_bones[bone_name].children:
                original_parents[child_bone.name] = bone_name
            #then remove so it doesn't clash when merged.
            merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])

    # Return to object mode
    bpy.ops.object.mode_set(mode='OBJECT')

    # Select and join armatures
    bpy.ops.object.select_all(action='DESELECT')
    base_armature.select_set(True)
    merge_armature.select_set(True)
    
    bpy.context.view_layer.objects.active = base_armature
    bpy.ops.object.join()

    # Explicitly set active object after join
    bpy.context.view_layer.objects.active = base_armature
    base_armature_data: bpy.types.Armature = base_armature.data

    # Restore parent relationships
    bpy.ops.object.mode_set(mode='EDIT')
    for bone in base_armature_data.edit_bones:
        if bone.name in original_parents:
            parent_name: Optional[str] = original_parents[bone.name]
            if parent_name:
                parent_bone: Optional[EditBone] = base_armature_data.edit_bones.get(parent_name)
                if parent_bone:
                    bone.parent = parent_bone

    bpy.ops.object.mode_set(mode='OBJECT')

    for mesh_obj in meshes_to_reparent:
        if mesh_obj and mesh_obj.name in bpy.data.objects:
            mesh_obj.parent = base_armature
            for mod in mesh_obj.modifiers:
                if mod.type == 'ARMATURE':
                    mod.object = base_armature

    # Process vertex groups if not mesh_only
    if not mesh_only:
        meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
        process_vertex_groups(meshes)

        # Remove zero weight vertex groups if enabled
        if bpy.context.scene.avatar_toolkit.remove_zero_weights:
            bpy.context.view_layer.objects.active = base_armature
            for mesh in meshes:
                bpy.context.view_layer.objects.active = mesh
                bpy.ops.avatar_toolkit.clean_weights()

    # Join meshes if requested
    if join_meshes:
        meshes_to_join: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
        if meshes_to_join:
            joined_mesh: Optional[Object] = join_mesh_objects(bpy.context, meshes_to_join)
            if joined_mesh:
                logger.info(f"Joined meshes into {joined_mesh.name}")
                # Ensure the joined mesh is properly parented
                joined_mesh.parent = base_armature

    # Clean up shape keys if enabled
    if bpy.context.scene.avatar_toolkit.cleanup_shape_keys:
        for obj in bpy.data.objects:
            if obj.type == 'MESH' and obj.parent == base_armature:
                remove_unused_shapekeys(obj)

    # Remove any remaining .merge bones
    bpy.context.view_layer.objects.active = base_armature
    bpy.ops.object.mode_set(mode='OBJECT')

    # Final cleanup
    clear_unused_data_blocks()

def validate_merge_armature_transforms(
    base_armature: Object,
    merge_armature: Object, 
    mesh_merge: Optional[Object],
    tolerance: float
) -> bool:
    """Validate transforms of both armatures and mesh"""
    for i in [0, 1, 2]:
        if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance:
            return False
            
        if abs(merge_armature.rotation_euler[i]) > tolerance or \
           (mesh_merge and abs(mesh_merge.rotation_euler[i]) > tolerance):
            return False
            
    return True

def adjust_merge_armature_transforms(
    merge_armature: Object,
    mesh_merge: Object
) -> None:
    """Adjust transforms of the merge armature"""
    old_loc: List[float] = list(merge_armature.location)
    old_scale: List[float] = list(merge_armature.scale)

    for i in [0, 1, 2]:
        merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i]
        merge_armature.rotation_euler[i] = mesh_merge.rotation_euler[i]
        merge_armature.scale[i] = mesh_merge.scale[i] * old_scale[i]

    for i in [0, 1, 2]:
        mesh_merge.location[i] = 0
        mesh_merge.rotation_euler[i] = 0
        mesh_merge.scale[i] = 1

def detect_bones_to_merge(
    base_edit_bones: bpy.types.ArmatureEditBones,
    merge_edit_bones: bpy.types.ArmatureEditBones,
    tolerance: float
) -> List[str]:
    """Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
    bones_to_merge: List[str] = []

    # Cache base bone positions
    base_bones_positions: Dict[str, np.ndarray] = {
        bone.name: np.array(bone.head) for bone in base_edit_bones
    }

    # Smart bone detection
    for merge_bone in merge_edit_bones:
        merge_bone_position: np.ndarray = np.array(merge_bone.head)
        found_match: bool = False

        if merge_bone.name in base_bones_positions:
            # If merging same bones by name
            bones_to_merge.append(merge_bone.name)
            found_match = True
        else:
            # Find bones with close positions
            for base_bone_name, base_bone_position in base_bones_positions.items():
                if np.linalg.norm(merge_bone_position - base_bone_position) <= tolerance:
                    bones_to_merge.append(base_bone_name)
                    found_match = True
                    break

        if not found_match:
            # Handle unmatched bones if needed
            pass

    return bones_to_merge

def process_vertex_groups(meshes: List[Object]) -> None:
    """Process vertex groups in meshes"""
    for mesh in meshes:
        vg_names: Set[str] = {vg.name for vg in mesh.vertex_groups}
        merge_vg_names: List[str] = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]

        for vg_merge_name in merge_vg_names:
            base_name: str = vg_merge_name[:-6]
            vg_merge: Optional[VertexGroup] = mesh.vertex_groups.get(vg_merge_name)
            vg_base: Optional[VertexGroup] = mesh.vertex_groups.get(base_name)

            if vg_merge is None:
                continue

            if vg_base:
                mix_vertex_groups(mesh, vg_merge_name, base_name)
            else:
                vg_merge.name = base_name

def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str) -> None:
    """Mix vertex group weights"""
    vg_from: Optional[VertexGroup] = mesh.vertex_groups.get(vg_from_name)
    vg_to: Optional[VertexGroup] = mesh.vertex_groups.get(vg_to_name)
        
    if not vg_from or not vg_to:
        return

    num_vertices: int = len(mesh.data.vertices)
    weights_from: np.ndarray = np.zeros(num_vertices)
    weights_to: np.ndarray = np.zeros(num_vertices)

    idx_from: int = vg_from.index
    idx_to: int = vg_to.index

    for v in mesh.data.vertices:
        for g in v.groups:
            if g.group == idx_from:
                weights_from[v.index] = g.weight
            elif g.group == idx_to:
                weights_to[v.index] = g.weight

    weights_combined: np.ndarray = np.clip(weights_from + weights_to, 0.0, 1.0)
    vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
    mesh.vertex_groups.remove(vg_from)

def apply_armature_to_mesh(armature: Object, mesh: Object) -> None:
    """Apply armature deformation to mesh"""
    armature_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
    armature_mod.object = armature
    
    if bpy.app.version >= (3, 5):
        mesh.modifiers.move(mesh.modifiers.find(armature_mod.name), 0)
    else:
        for _ in range(len(mesh.modifiers) - 1):
            bpy.ops.object.modifier_move_up(modifier=armature_mod.name)
            
    with bpy.context.temp_override(object=mesh):
        bpy.ops.object.modifier_apply(modifier=armature_mod.name)

def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context) -> None:
    """Apply armature deformation to mesh with shape keys"""
    old_active_index: int = mesh.active_shape_key_index
    old_show_only: bool = mesh.show_only_shape_key
    mesh.show_only_shape_key = True
    
    shape_keys: List[ShapeKey] = mesh.data.shape_keys.key_blocks
    vertex_groups: List[str] = []
    mutes: List[bool] = []
    
    for sk in shape_keys:
        vertex_groups.append(sk.vertex_group)
        sk.vertex_group = ''
        mutes.append(sk.mute)
        sk.mute = False

    disabled_mods: List[Any] = []
    for mod in mesh.modifiers:
        if mod.show_viewport:
            mod.show_viewport = False
            disabled_mods.append(mod)

    arm_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
    arm_mod.object = armature
    
    co_length: int = len(mesh.data.vertices) * 3
    eval_cos: np.ndarray = np.empty(co_length, dtype=np.single)
    
    for i, shape_key in enumerate(shape_keys):
        mesh.active_shape_key_index = i
        
        depsgraph = context.evaluated_depsgraph_get()
        eval_mesh: Mesh = mesh.evaluated_get(depsgraph)
        eval_mesh.data.vertices.foreach_get('co', eval_cos)
        
        shape_key.data.foreach_set('co', eval_cos)
        if i == 0:
            mesh.data.vertices.foreach_set('co', eval_cos)

    for mod in disabled_mods:
        mod.show_viewport = True
        
    mesh.modifiers.remove(arm_mod)
    
    for sk, vg, mute in zip(shape_keys, vertex_groups, mutes):
        sk.vertex_group = vg
        sk.mute = mute
        
    mesh.active_shape_key_index = old_active_index
    mesh.show_only_shape_key = old_show_only
