import bpy, sys, linecache, ast
import math
from math import *
from mathutils import *
from bpy.types import Panel, UIList
from .utils import *
from .define import *


# OPERATOR CLASSES
##################
class MR_OT_update(bpy.types.Operator):
    """Update old control rig to Blender 3.0"""

    bl_idname = "mr.update"
    bl_label = "update"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        if context.active_object:
            if context.active_object.type == "ARMATURE":
                return "mr_control_rig" in context.active_object.data.keys()



    def execute(self, context):
        try:
            _update(self)
        finally:
            pass

        return {'FINISHED'}



class MR_OT_exportGLTF(bpy.types.Operator):
    """Export to GLTF format"""

    bl_idname = "mr.export_gltf"
    bl_label = "export_gltf"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        if context.active_object:
            if context.active_object.type == "ARMATURE":
                return True


    def execute(self, context):
        try:
            bpy.ops.export_scene.gltf()
        finally:
            pass

        return {'FINISHED'}


class MR_OT_apply_shape(bpy.types.Operator):
    """Apply the selected shape"""

    bl_idname = "mr.apply_shape"
    bl_label = "apply_shape"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        if context.active_object:
            if context.mode == 'EDIT_MESH':
                if 'cs_user' in context.active_object.name:
                    return True

    def execute(self, context):
        use_global_undo = context.preferences.edit.use_global_undo
        context.preferences.edit.use_global_undo = False

        try:
            _apply_shape()
        finally:
            context.preferences.edit.use_global_undo = use_global_undo
        return {'FINISHED'}


class MR_OT_edit_custom_shape(bpy.types.Operator):
    """Edit the selected bone shape"""

    bl_idname = "mr.edit_custom_shape"
    bl_label = "edit_custom_shape"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        if context.mode == 'POSE':
            if bpy.context.active_pose_bone:
                return True

    def execute(self, context):
        try:
            cs = bpy.context.active_pose_bone.custom_shape
            if cs:
                _edit_custom_shape()
            else:
                self.report({"ERROR"}, "No custom shapes set for this bone.")

        finally:
            pass

        return {'FINISHED'}


class MR_OT_make_rig(bpy.types.Operator):
    """Generate a control rig from the selected Mixamo skeleton"""

    bl_idname = "mr.make_rig"
    bl_label = "Create control rig from selected armature"
    bl_options = {'UNDO'}

    bake_anim: bpy.props.BoolProperty(name="Bake Anim", description="Bake animation to the control bones", default=True)
    ik_arms: bpy.props.BoolProperty(name="IK Hands", description="Use IK for arm bones, otherwise use FK (can be toggled later using the rig properties)", default=True)
    ik_legs: bpy.props.BoolProperty(name="IK Legs", description="Use IK for leg bones, otherwise use FK (can be toggled later using the rig properties)", default=True)
    animated_armature = None

    @classmethod
    def poll(cls, context):
        if context.active_object:
            if context.active_object.type == "ARMATURE":
                if not "mr_control_rig" in context.active_object.data.keys():
                    return True
        return False


    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self, width=450)


    def draw(self, context):
        layout = self.layout
        layout.prop(self, 'bake_anim', text="Apply Animation")
        layout.prop(self, 'ik_arms', text="IK Arms")
        layout.prop(self, 'ik_legs', text="IK Legs")


    def execute(self, context):
        debug = False
        # ~ layer_select = []

        try:
            # only select the armature
            arm = get_object(context.active_object.name)
            bpy.ops.object.mode_set(mode='OBJECT')
            bpy.ops.object.select_all(action='DESELECT')
            set_active_object(arm.name)

            # enable all armature layers
            layer_select = enable_all_armature_layers()

            # animation import: initial steps
            if self.bake_anim:
                if not "mr_control_rig" in arm.data.keys():# only if the control rig is not already built
                    # duplicate current skeleton
                    duplicate_object()
                    copy_name = arm.name+"_TEMPANIM"
                    self.animated_armature = get_object(bpy.context.active_object.name)
                    self.animated_armature.name = copy_name
                    self.animated_armature["mix_to_del"] = True

                    bpy.ops.object.mode_set(mode='OBJECT')
                    bpy.ops.object.select_all(action='DESELECT')
                    set_active_object(arm.name)

            # set to rest pose, clear animation
            _zero_out()

            # build control rig
            _make_rig(self)

            if blender_version._float < 291:
                # Child Of constraints inverse matrix must be set manually in Blender versions < 2.91
                print("Set inverse ChildOf")
                _reset_inverse_constraints()

            # animation import: retarget
            if self.bake_anim and self.animated_armature:
                _import_anim(self.animated_armature, arm)

            # set KeyingSet
            ks = context.scene.keying_sets_all
            try:
                ks.active = ks["Location & Rotation"]
            except:# doesn't exist in older Blender versions
                pass


        finally:
            bpy.ops.object.mode_set(mode='OBJECT')
            bpy.ops.object.select_all(action='DESELECT')
            set_active_object(arm.name)

            if debug == False:
                restore_armature_layers(layer_select)
                remove_retarget_cns(bpy.context.active_object)
                remove_temp_objects()
                clean_scene()


            self.report({"INFO"}, "Control Rig Done!")

        return {'FINISHED'}


class MR_OT_zero_out(bpy.types.Operator):
    """Delete all keys and set every bones to (0,0,0) rotation"""

    bl_idname = "mr.zero_out"
    bl_label = "zero_out"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        if context.active_object:
            return context.active_object.type == "ARMATURE"
        return False


    def execute(self, context):
        scn = bpy.context.scene


        try:
            _zero_out()

        finally:
            print("")


        return {'FINISHED'}


class MR_OT_bake_anim(bpy.types.Operator):
    """Merge all animation layers (see NLA editor) into a single layer"""

    bl_idname = "mr.bake_anim"
    bl_label = "bake_anim"
    bl_options = {'UNDO'}


    @classmethod
    def poll(cls, context):
        if context.active_object:
            return context.active_object.type == "ARMATURE"
        return False


    def execute(self, context):
        scn = bpy.context.scene

        try:
            _bake_anim(self)

        finally:
            pass


        return {'FINISHED'}


class MR_OT_import_anim(bpy.types.Operator):
    """Import an animation file (FBX) of the same character to the control rig"""

    bl_idname = "mr.import_anim_to_rig"
    bl_label = "import_anim_to_rig"
    bl_options = {'UNDO'}


    @classmethod
    def poll(cls, context):
        if context.active_object:
            if context.active_object.type == "ARMATURE":
                if "mr_control_rig" in context.active_object.data.keys():
                    return True
        return False


    def execute(self, context):
        scn = bpy.context.scene
        debug = False
        error = False
        layer_select = []

        if scn.mix_source_armature == None:
            self.report({'ERROR'}, "Source armature must be set")
            return {'FINISHED'}


        try:
            layer_select = enable_all_armature_layers()
            #tar_arm = scn.mix_target_armature
            tar_arm = get_object(bpy.context.active_object.name)
            #src_arm = [i for i in bpy.context.selected_objects if i != tar_arm][0]
            src_arm = scn.mix_source_armature
            print("Source", src_arm.name)
            print("Target", tar_arm.name)

            _import_anim(src_arm, tar_arm, import_only=True)

        #except:
        #    error = True
        #    print("Error")

        finally:
            if debug == False:
                restore_armature_layers(layer_select)
                remove_retarget_cns(bpy.context.active_object)

                if scn.mix_source_armature:
                    try:
                        remove_retarget_cns(mix_source_armature)
                    except:
                        pass

                remove_temp_objects()

            self.report({"INFO"}, "Animation imported")


        return {'FINISHED'}




# OPERATOR FUNCTIONS
#####################

def _apply_shape():
    bpy.ops.object.mode_set(mode='OBJECT')
    obj = bpy.context.active_object
    obj_name = obj.name
    shape = bpy.data.objects.get(obj_name)
    delete_obj = False

    cs_grp = get_object('cs_grp')
    if cs_grp:
        shape.parent = bpy.data.objects['cs_grp']

    mr_armature_name = None
    mr_armature = None

    if len(shape.keys()) > 0:
        for key in shape.keys():
            if 'delete' in shape.keys():
                delete_obj = True
            if 'mr_armature' in key:
                mr_armature_name = shape['mr_armature']
                mr_armature = bpy.data.objects.get(mr_armature_name)

    if delete_obj:
        bpy.ops.object.delete(use_global=False)
    else:
        # assign to collection
        if mr_armature:
            if len(mr_armature.users_collection) > 0:
                for collec in mr_armature.users_collection:
                    if len(collec.name.split('_')) == 1:
                        continue
                    if collec.name.split('_')[1] == "rig" or collec.name.split('_')[1] == "grp":
                        cs_collec = bpy.data.collections.get(collec.name.split('_')[0] + '_cs')
                        if cs_collec:
                            # remove from root collection
                            if bpy.context.scene.collection.objects.get(shape.name):
                                bpy.context.scene.collection.objects.unlink(shape)
                            # remove from other collections
                            for other_collec in shape.users_collection:
                                other_collec.objects.unlink(shape)
                            # assign to cs collection
                            cs_collec.objects.link(shape)
                            print("assigned to collec", cs_collec.name)
                        else:
                            print("cs collec not found")
                    else:
                        print("rig collec not found")

            else:
                print("Armature has no collection")
        else:
            print("Armature not set")

    # hide shape
    try:
        hide_object(shape)
    except:  # weird error 'StructRNA of type Object has been removed'
        print("Error, could not hide shape")
        pass

    if mr_armature:
        set_active_object(mr_armature.name)
        bpy.ops.object.mode_set(mode='POSE')


def _edit_custom_shape():
    bone = bpy.context.active_pose_bone
    rig_name = bpy.context.active_object.name
    rig = get_object(rig_name)

    cs = bone.custom_shape
    cs_mesh = cs.data

    bpy.ops.object.posemode_toggle()

    # make sure the active collection is not hidden, otherwise we can't access the newly created object data
    active_collec = bpy.context.layer_collection
    if not active_collec.is_visible:
        for col in rig.users_collection:
            layer_col = search_layer_collection(bpy.context.view_layer.layer_collection, col.name)
            if layer_col.hide_viewport == False and col.hide_viewport == False:
                bpy.context.view_layer.active_layer_collection = layer_col
                break

    # create new mesh data
    bpy.ops.mesh.primitive_plane_add(size=1, enter_editmode=False, location=(-0, 0, 0.0), rotation=(0.0, 0.0, 0.0))

    mesh_obj = bpy.context.active_object
    mesh_obj.name = 'cs_user_' + bone.name

    if cs.name == "cs_user_" + bone.name:# make a mesh instance if it's a already edited
        mesh_obj.data = cs_mesh
        mesh_obj['delete'] = 1.0
    else:  # else create new object data
        mesh_obj.data = cs_mesh.copy()
        mesh_obj.data.name = mesh_obj.name
        bone.custom_shape = mesh_obj

    # store the current armature name in a custom prop
    mesh_obj['mr_armature'] = rig_name

    if bone.custom_shape_transform:
        bone_transf = bone.custom_shape_transform
        mesh_obj.matrix_world = rig.matrix_world @ bone_transf.matrix
    else:
        mesh_obj.matrix_world = rig.matrix_world @ bone.matrix

    mesh_obj.scale *= get_custom_shape_scale(bone)
    mesh_obj.scale *= bone.length

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


def clean_scene():
    # hide cs_grp
    cs_grp = get_object("cs_grp")
    if cs_grp:
        for c in cs_grp.children:
            hide_object(c)
        hide_object(cs_grp)

    # Display layer 0 and 1 only
    # ~ layers = bpy.context.active_object.data.layers
    # ~ layers[0] = True
    # ~ for i in range(0, 32):
        # ~ layers[i] = i in [0]

    for c in bpy.context.active_object.data.collections:
        if c.name in ('CTRL'):
            c.is_visible = True
        else:
            c.is_visible = False


def init_armature_transforms(rig):
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(rig.name)
    bpy.ops.object.mode_set(mode='OBJECT')

    # first unparent children meshes (init scale messed up children scale in Blender 2.8)
    child_par_dict = {}
    for child in bpy.data.objects[rig.name].children:
        bone_parent = None
        if child.parent_type == "BONE":
            bone_parent = child.parent_bone
        child_par_dict[child.name] = bone_parent
        child_mat = child.matrix_world.copy()
        child.parent = None
        bpy.context.evaluated_depsgraph_get().update()
        child.matrix_world = child_mat

    # apply armature transforms
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    bpy.context.evaluated_depsgraph_get().update()

    # restore armature children
    for child_name in child_par_dict:
        child = bpy.data.objects.get(child_name)
        child_mat = child.matrix_world.copy()
        child.parent = bpy.data.objects[rig.name]
        if child_par_dict[child_name] != None:# bone parent
            child.parent_type = "BONE"
            child.parent_bone = child_par_dict[child_name]

        bpy.context.evaluated_depsgraph_get().update()
        child.matrix_world = child_mat


def _reset_inverse_constraints():
    bpy.ops.object.mode_set(mode='POSE')

    rig_name = bpy.context.active_object.name
    rig = get_object(rig_name)

    for pb in rig.pose.bones:
        if len(pb.constraints):
            for cns in pb.constraints:
                if cns.type == 'CHILD_OF':
                    set_constraint_inverse_matrix(cns)


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



def _update(self):
    if blender_version._float >= 300:
        convert_drivers_cs_to_xyz(bpy.context.active_object)


def _make_rig(self):
    print("\nBuilding control rig...")

    scn = bpy.context.scene
    rig_name = bpy.context.active_object.name
    rig = get_object(rig_name)

    coll_mix_name = "DEF"
    coll_ctrl_name = "CTRL"
    coll_intern_name = "MCH"

    # ~ _ = rig.data.collections.new(coll_0_name)
    # ~ for eb in rig.data.edit_bones:
        # ~ set_bone_collection(rig, eb, coll_0_name)

    # ~ layer_mix_idx = 1
    # ~ layer_ctrl_idx = 0
    # ~ layer_intern_idx = 8
    use_name_prefix = True

    c_master_name = c_prefix+master_rig_names["master"]

    # Init transforms
    init_armature_transforms(rig)


    def add_master():
        print("  Add Master")

        # -- Edit --
        bpy.ops.object.mode_set(mode='EDIT')


        # Create bones
        c_master = create_edit_bone(c_master_name)
        c_master.head = [0, 0, 0]
        c_master.tail = [0, 0, 0.05 * rig.dimensions[2]]
        c_master.roll = 0.01
        set_bone_collection(rig, c_master, coll_ctrl_name)
        # ~ set_bone_layer(c_master, layer_ctrl_idx)
        c_master["mixamo_ctrl"] = 1# tag as controller bone


        # -- Pose --
        bpy.ops.object.mode_set(mode='POSE')


        c_master_pb = get_pose_bone(c_master_name)

        # set custom shapes
        set_bone_custom_shape(c_master_pb, "cs_master")

        # set rotation mode
        c_master_pb.rotation_mode = "XYZ"

        # set color group
        set_bone_color_group(rig, c_master_pb, "master")


    def add_spine():
        print("  Add Spine")

        # -- Edit --
        bpy.ops.object.mode_set(mode='EDIT')


        # Create bones
        hips_name = get_mix_name(spine_names["pelvis"], use_name_prefix)
        spine_name = get_mix_name(spine_names["spine1"], use_name_prefix)
        spine1_name = get_mix_name(spine_names["spine2"], use_name_prefix)
        spine2_name = get_mix_name(spine_names["spine3"], use_name_prefix)

        hips = get_edit_bone(hips_name)
        spine = get_edit_bone(spine_name)
        spine1 = get_edit_bone(spine1_name)
        spine2 = get_edit_bone(spine2_name)

        if not hips or not spine or not spine1 or not spine2:
            print("  Spine bones are missing, skip spine")
            return

        for b in [hips, spine, spine1, spine2]:
            set_bone_collection(rig, b, coll_mix_name)
            # ~ set_bone_layer(b, layer_mix_idx)

        # Hips Ctrl
        c_hips_name = c_prefix+spine_rig_names["pelvis"]
        c_hips = create_edit_bone(c_hips_name)
        copy_bone_transforms(hips, c_hips)
        c_hips.parent = get_edit_bone(c_prefix+master_rig_names["master"])
        set_bone_collection(rig, c_hips, coll_ctrl_name)
        # ~ set_bone_layer(c_hips, layer_ctrl_idx)
        c_hips["mixamo_ctrl"] = 1# tag as controller bone


        # Free Hips Ctrl
        c_hips_free_name = c_prefix+spine_rig_names["hips_free"]
        c_hips_free = create_edit_bone(c_hips_free_name)
        c_hips_free.head = hips.tail.copy()
        c_hips_free.tail = hips.head.copy()
        align_bone_x_axis(c_hips_free,  hips.x_axis)
        c_hips_free["mixamo_ctrl"] = 1# tag as controller bone

        c_hips_free.parent = c_hips
        set_bone_collection(rig, c_hips_free, coll_ctrl_name)
        # ~ set_bone_layer(c_hips_free, layer_ctrl_idx)

        # Free Hips helper
        hips_free_h_name = spine_rig_names["hips_free_helper"]
        hips_free_helper = create_edit_bone(hips_free_h_name)
        copy_bone_transforms(hips, hips_free_helper)
        hips_free_helper.parent = c_hips_free
        set_bone_collection(rig, hips_free_helper, coll_intern_name)
        # ~ set_bone_layer(hips_free_helper, layer_intern_idx)

        # Spine Ctrl
        c_spine_name = c_prefix+spine_rig_names["spine1"]
        c_spine = create_edit_bone(c_spine_name)
        copy_bone_transforms(spine, c_spine)
        c_spine.parent = c_hips
        # ~ set_bone_layer(c_spine, layer_ctrl_idx)
        set_bone_collection(rig, c_spine, coll_ctrl_name)
        c_spine["mixamo_ctrl"] = 1# tag as controller bone

        # Spine1 Ctrl
        c_spine1_name = c_prefix+spine_rig_names["spine2"]
        c_spine1 = create_edit_bone(c_spine1_name)
        copy_bone_transforms(spine1, c_spine1)
        c_spine1.parent = c_spine
        set_bone_collection(rig, c_spine1, coll_ctrl_name)
        # ~ set_bone_layer(c_spine1, layer_ctrl_idx)
        c_spine1["mixamo_ctrl"] = 1# tag as controller bone

        # Spine2 Ctrl
        c_spine2_name = c_prefix+spine_rig_names["spine3"]
        c_spine2 = create_edit_bone(c_spine2_name)
        copy_bone_transforms(spine2, c_spine2)
        c_spine2.parent = c_spine1
        set_bone_collection(rig, c_spine2, coll_ctrl_name)
        # ~ set_bone_layer(c_spine2, layer_ctrl_idx)
        c_spine2["mixamo_ctrl"] = 1# tag as controller bone


        # -- Pose --
        bpy.ops.object.mode_set(mode='POSE')


        c_hips_pb = get_pose_bone(c_hips_name)
        hips_helper_pb = get_pose_bone(hips_free_h_name)
        c_hips_free_pb = get_pose_bone(c_hips_free_name)
        c_spine_pb = get_pose_bone(c_spine_name)
        c_spine1_pb = get_pose_bone(c_spine1_name)
        c_spine2_pb = get_pose_bone(c_spine2_name)

        # set custom shapes
        set_bone_custom_shape(c_hips_pb, "cs_square_2")
        set_bone_custom_shape(c_hips_free_pb, "cs_hips")
        set_bone_custom_shape(c_spine_pb, "cs_circle")
        set_bone_custom_shape(c_spine1_pb, "cs_circle")
        set_bone_custom_shape(c_spine2_pb, "cs_circle")

        # set rotation mode
        c_hips_pb.rotation_mode = "XYZ"
        c_hips_free_pb.rotation_mode = "XYZ"
        c_spine_pb.rotation_mode = "XYZ"
        c_spine1_pb.rotation_mode = "XYZ"
        c_spine2_pb.rotation_mode = "XYZ"

        # set color group
        set_bone_color_group(rig, c_hips_pb, "root_master")
        set_bone_color_group(rig, c_hips_free_pb, "body_mid")
        set_bone_color_group(rig, c_spine_pb, "body_mid")
        set_bone_color_group(rig, c_spine1_pb, "body_mid")
        set_bone_color_group(rig, c_spine2_pb, "body_mid")

        # constraints
            # Hips
        mixamo_spine_pb = get_pose_bone(hips_name)
        cns = mixamo_spine_pb.constraints.get("Copy Transforms")
        if cns == None:
            cns = mixamo_spine_pb.constraints.new("COPY_TRANSFORMS")
            cns.name = "Copy Transforms"
        cns.target = rig
        cns.subtarget = hips_free_h_name

            # Spine
        spine_bone_matches = {"1": c_spine_name, "2":c_spine1_name, "3":c_spine2_name}
        for str_idx in spine_bone_matches:
            c_name = spine_bone_matches[str_idx]
            mixamo_bname = get_mix_name(spine_names["spine"+str_idx], use_name_prefix)
            mixamo_spine_pb = get_pose_bone(mixamo_bname)
            cns = mixamo_spine_pb.constraints.get("Copy Transforms")
            if cns == None:
                cns = mixamo_spine_pb.constraints.new("COPY_TRANSFORMS")
                cns.name = "Copy Transforms"
            cns.target = rig
            cns.subtarget = c_name


    def add_head():
        print("  Add Head")

        # -- Edit --
        bpy.ops.object.mode_set(mode='EDIT')


        # Create bones
        neck_name = get_mix_name(head_names["neck"], use_name_prefix)
        head_name = get_mix_name(head_names["head"], use_name_prefix)
        head_end_name = get_mix_name(head_names["head_end"], use_name_prefix)

        neck = get_edit_bone(neck_name)
        head = get_edit_bone(head_name)
        head_end = get_edit_bone(head_end_name)

        if not neck or not head:
            print("  Head or neck bones are missing, skip head")
            return

        for b in [neck, head, head_end]:
            set_bone_collection(rig, b, coll_mix_name)
            # ~ set_bone_layer(b, layer_mix_idx)

        # Neck Ctrl
        c_neck_name = c_prefix+head_rig_names["neck"]
        c_neck = create_edit_bone(c_neck_name)
        copy_bone_transforms(neck, c_neck)
        c_neck.parent = get_edit_bone(c_prefix+spine_rig_names["spine3"])
        # ~ set_bone_layer(c_neck, layer_ctrl_idx)
        set_bone_collection(rig, c_neck, coll_ctrl_name)
        c_neck["mixamo_ctrl"] = 1# tag as controller bone

        # Head Ctrl
        c_head_name = c_prefix+head_rig_names["head"]
        c_head = create_edit_bone(c_head_name)
        copy_bone_transforms(head, c_head)
        c_head.parent = c_neck
        # ~ set_bone_layer(c_head, layer_ctrl_idx)
        set_bone_collection(rig, c_head, coll_ctrl_name)
        c_head["mixamo_ctrl"] = 1# tag as controller bone

        # -- Pose --
        bpy.ops.object.mode_set(mode='POSE')


        c_neck_pb = get_pose_bone(c_neck_name)
        c_head_pb = get_pose_bone(c_head_name)

        # set custom shapes
        set_bone_custom_shape(c_neck_pb, "cs_neck")
        set_bone_custom_shape(c_head_pb, "cs_head")

        # set rotation mode
        c_neck_pb.rotation_mode = "XYZ"
        c_head_pb.rotation_mode = "XYZ"

        # set color group
        set_bone_color_group(rig, c_neck_pb, "neck")
        set_bone_color_group(rig, c_head_pb, "head")

        # constraints
            # Neck
        neck_pb = get_pose_bone(neck_name)
        head_pb = get_pose_bone(head_name)

        add_copy_transf(neck_pb, rig, c_neck_name)
        add_copy_transf(head_pb, rig, c_head_name)


    def add_leg(side):
        print("  Add Leg", side)

        _side = "_"+side
        thigh_name = get_mix_name(side+leg_names["thigh"], use_name_prefix)
        calf_name = get_mix_name(side+leg_names["calf"], use_name_prefix)
        foot_name = get_mix_name(side+leg_names["foot"], use_name_prefix)
        toe_name = get_mix_name(side+leg_names["toes"], use_name_prefix)
        toe_end_name = get_mix_name(side+leg_names["toes_end"], use_name_prefix)

        # -- Edit --
        bpy.ops.object.mode_set(mode='EDIT')


        thigh = get_edit_bone(thigh_name)
        calf = get_edit_bone(calf_name)
        foot = get_edit_bone(foot_name)
        toe = get_edit_bone(toe_name)
        toe_end = get_edit_bone(toe_end_name)

        hips = get_edit_bone(get_mix_name(spine_names["pelvis"], use_name_prefix))
        c_hips_free_name = c_prefix+spine_rig_names["hips_free"]
        c_hips_free = get_edit_bone(c_hips_free_name)

        if not thigh or not calf or not foot or not toe:
            print("  Leg bones are missing, skip leg: "+side)
            return

        # Set Mixamo bones in layer
        for b in [thigh, calf, foot, toe, toe_end]:
            set_bone_collection(rig, b, coll_mix_name)
            # ~ set_bone_layer(b, layer_mix_idx)

        # Create bones
        # correct straight leg angle, need minimum 0.1 degrees for IK constraints to work
        def get_leg_angle():
            #return degrees(thigh.y_axis.angle(calf.y_axis))
            vec1 = calf.head - thigh.head
            vec2 = foot.head - calf.head
            return degrees(vec1.angle(vec2))

        leg_angle = get_leg_angle()

        if leg_angle < 0.1:
            print("  ! Straight leg bones, angle = "+str(leg_angle))
            max_iter = 10000
            i = 0

            while leg_angle < 0.1 and i < max_iter:

                dir = ((thigh.z_axis + calf.z_axis)*0.5).normalized()
                calf.head += dir * (calf.tail-calf.head).magnitude * 0.0001
                leg_angle = get_leg_angle()
                i += 1

            print("    corrected leg angle: "+str(leg_angle))


        # Thigh IK
        thigh_ik_name = leg_rig_names["thigh_ik"]+_side
        thigh_ik = create_edit_bone(thigh_ik_name)
        copy_bone_transforms(thigh, thigh_ik)

        # auto-align knee position with global Y axis to ensure IK pole vector is physically correct
        leg_axis = calf.tail - thigh.head
        leg_midpoint = (thigh.head + calf.tail) * 0.5

        #cur_vec = calf.head - leg_midpoint
        #cur_vec[2] = 0.0
        #global_y_vec = Vector((0, -1, 0))

        dir = calf.head - leg_midpoint
        cur_vec = project_vector_onto_plane(dir, leg_axis)
        global_y_vec = project_vector_onto_plane(Vector((0, -1, 0)), leg_axis)

        signed_cur_angle = signed_angle(cur_vec, global_y_vec, leg_axis)
        print("  IK base angle:", degrees(signed_cur_angle))

        # rotate
        rotated_point = rotate_point(calf.head.copy(), -signed_cur_angle, leg_midpoint, leg_axis)

        # (check)
        dir = rotated_point - leg_midpoint
        cur_vec = project_vector_onto_plane(dir, leg_axis)
        signed_cur_angle = signed_angle(cur_vec, global_y_vec, leg_axis)
        print("    IK corrected angle:", degrees(signed_cur_angle))

        thigh_ik.tail = rotated_point

        thigh_ik.parent = c_hips_free
        set_bone_collection(rig, thigh_ik, coll_intern_name)
        # ~ set_bone_layer(thigh_ik, layer_intern_idx)

        # Thigh FK Ctrl
        c_thigh_fk_name = c_prefix+leg_rig_names["thigh_fk"]+_side
        c_thigh_fk = create_edit_bone(c_thigh_fk_name)
        copy_bone_transforms(thigh_ik, c_thigh_fk)
        c_thigh_fk.parent = c_hips_free
        # ~ set_bone_layer(c_thigh_fk, layer_ctrl_idx)
        set_bone_collection(rig, c_thigh_fk, coll_ctrl_name)
        c_thigh_fk["mixamo_ctrl"] = 1# tag as controller bone

        # Calf IK
        calf_ik_name = leg_rig_names["calf_ik"]+_side

        # check if bone exist to avoid undesired transformation when running the function multiple time
        calf_ik_exist = get_edit_bone(calf_ik_name)

        calf_ik = create_edit_bone(calf_ik_name)
        if calf_ik_exist == None:
            copy_bone_transforms(calf, calf_ik)
        calf_ik.head = thigh_ik.tail.copy()
        calf_ik.tail = foot.head.copy()
        calf_ik.parent = thigh_ik
        calf_ik.use_connect = True
        # ~ set_bone_layer(calf_ik, layer_intern_idx)
        set_bone_collection(rig, calf_ik, coll_intern_name)

        # align thigh and calf IK roll
            # align calf_ik local Z
        align_bone_z_axis(calf_ik, (calf_ik.head-leg_midpoint))
            # align thigh_ik on calf_ik
        align_bone_z_axis(thigh_ik, calf_ik.z_axis)
            # copy thigh_ik to c_thigh_fk
        copy_bone_transforms(thigh_ik, c_thigh_fk)


        # Calf FK Ctrl
        c_calf_fk_name = c_prefix+leg_rig_names["calf_fk"]+_side
        c_calf_fk = create_edit_bone(c_calf_fk_name)
        copy_bone_transforms(calf_ik, c_calf_fk)
        c_calf_fk.parent = c_thigh_fk
        set_bone_collection(rig, c_calf_fk, coll_ctrl_name)
        # ~ set_bone_layer(c_calf_fk, layer_ctrl_idx)
        c_calf_fk["mixamo_ctrl"] = 1# tag as controller bone

        # Foot FK Ctrl
        c_foot_fk_name = c_prefix+leg_rig_names["foot_fk"]+_side
        c_foot_fk = create_edit_bone(c_foot_fk_name)
        copy_bone_transforms(foot, c_foot_fk)
        c_foot_fk.tail[2] = foot.head[2]
        align_bone_z_axis(c_foot_fk, Vector((0,0,1)))
        c_foot_fk.parent = c_calf_fk
        # ~ set_bone_layer(c_foot_fk, layer_ctrl_idx)
        set_bone_collection(rig, c_foot_fk, coll_ctrl_name)
        c_foot_fk["mixamo_ctrl"] = 1# tag as controller bone

        # Foot FK
        foot_fk_name = leg_rig_names["foot_fk"]+_side
        foot_fk = create_edit_bone(foot_fk_name)
        copy_bone_transforms(foot, foot_fk)
        foot_fk.parent = c_foot_fk
        set_bone_collection(rig, foot_fk, coll_intern_name)
        # ~ set_bone_layer(foot_fk, layer_intern_idx)

        # Foot IK Ctrl
        c_foot_ik_name = c_prefix+leg_rig_names["foot_ik"]+_side
        c_foot_ik = create_edit_bone(c_foot_ik_name)
        copy_bone_transforms(foot, c_foot_ik)
        c_foot_ik.tail[2] = foot.head[2]
        align_bone_z_axis(c_foot_ik, Vector((0,0,1)))
        set_bone_collection(rig, c_foot_ik, coll_ctrl_name)
        # ~ set_bone_layer(c_foot_ik, layer_ctrl_idx)
        c_foot_ik["mixamo_ctrl"] = 1# tag as controller bone

        # Foot IK
        foot_ik_name = leg_rig_names["foot_ik"]+_side
        foot_ik = create_edit_bone(foot_ik_name)
        copy_bone_transforms(foot, foot_ik)
        foot_ik.parent = c_foot_ik
        set_bone_collection(rig, foot_ik, coll_intern_name)
        # ~ set_bone_layer(foot_ik, layer_intern_idx)

        # Foot Snap
        foot_snap_name = leg_rig_names["foot_snap"]+_side
        foot_snap = create_edit_bone(foot_snap_name)
        copy_bone_transforms(c_foot_ik, foot_snap)
        foot_snap.parent = foot_ik
        set_bone_collection(rig, foot_snap, coll_intern_name)
        # ~ set_bone_layer(foot_snap, layer_intern_idx)

        # Foot IK target
        foot_ik_target_name = leg_rig_names["foot_ik_target"]+_side
        foot_ik_target = create_edit_bone(foot_ik_target_name)
        foot_ik_target.head = foot_ik.head.copy()
        foot_vec = (foot.tail - foot.head)
        foot_ik_target.tail = foot_ik_target.head - (foot_vec*0.25)
        align_bone_z_axis(foot_ik_target, Vector((0,0,1)))
        # parent set below (c_foot_01)
        # ~ set_bone_layer(foot_ik_target, layer_intern_idx)
        set_bone_collection(rig, foot_ik_target, coll_intern_name)

        # Foot Heel Out
        heel_out_name = leg_rig_names["heel_out"]+_side
        heel_out = create_edit_bone(heel_out_name)
        heel_out.head, heel_out.tail = Vector((0,0,0)), Vector((0,0,1))
        heel_out.parent = c_foot_ik
        # ~ set_bone_layer(heel_out, layer_intern_idx)
        set_bone_collection(rig, heel_out, coll_intern_name)

        # Foot Heel In
        heel_in_name = leg_rig_names["heel_in"]+_side
        heel_in = create_edit_bone(heel_in_name)
        heel_in.head, heel_in.tail = Vector((0,0,0)), Vector((0,0,1))
        heel_in.parent = heel_out
        # ~ set_bone_layer(heel_in, layer_intern_idx)
        set_bone_collection(rig, heel_in, coll_intern_name)

        # Foot Heel Mid
        heel_mid_name = leg_rig_names["heel_mid"]+_side
        heel_mid = create_edit_bone(heel_mid_name)
        heel_mid.head, heel_mid.tail = Vector((0,0,0)), Vector((0,0,1))
        heel_mid.parent = heel_in
        # ~ set_bone_layer(heel_mid, layer_intern_idx)
        set_bone_collection(rig, heel_mid, coll_intern_name)

        heel_mid.head[0], heel_mid.head[1], heel_mid.head[2] = foot.head[0], foot.head[1], foot.tail[2]
        heel_mid.tail = foot.tail.copy()
        heel_mid.tail[2] = heel_mid.head[2]
        heel_mid.tail = heel_mid.head + (heel_mid.tail-heel_mid.head)*0.5
        align_bone_x_axis(heel_mid, foot.x_axis)

        copy_bone_transforms(heel_mid, heel_in)
        # use the foot x axis to determine "inside" vector, make sure it's pointing in the right direction for right and left side
        fac = 1
        if side == "Right":
            fac = -1

        heel_in.head += foot.x_axis.normalized() * foot.length*0.3 * fac
        heel_in.tail += foot.x_axis.normalized() * foot.length*0.3 * fac

        copy_bone_transforms(heel_mid, heel_out)
        heel_out.head += foot.x_axis.normalized() * foot.length*0.3 * -fac
        heel_out.tail += foot.x_axis.normalized() * foot.length*0.3 * -fac

        # Toe End
        toes_end_name = leg_rig_names["toes_end"]+_side
        toes_end = create_edit_bone(toes_end_name)
        copy_bone_transforms(toe, toes_end)
        toe_vec = (toes_end.tail-toes_end.head)
        toes_end.tail += toe_vec
        toes_end.head += toe_vec
        toes_end.parent = heel_mid
        # ~ set_bone_layer(toes_end, layer_intern_idx)
        set_bone_collection(rig, toes_end, coll_intern_name)

        # Toe End 01
        toes_end_01_name = leg_rig_names["toes_end_01"]+_side
        toes_end_01 = create_edit_bone(toes_end_01_name)
        copy_bone_transforms(toes_end, toes_end_01)
        vec = toes_end_01.tail -toes_end_01.head
        toes_end_01.tail = toes_end_01.head + (vec*0.5)
        toes_end_01.parent = toes_end
        # ~ set_bone_layer(toes_end_01, layer_intern_idx)
        set_bone_collection(rig, toes_end_01, coll_intern_name)

        # Foot 01 Ctrl
        c_foot_01_name = c_prefix+leg_rig_names["foot_01"]+_side
        c_foot_01 = create_edit_bone(c_foot_01_name)
        copy_bone_transforms(foot, c_foot_01)
        c_foot_01_vec = c_foot_01.tail - c_foot_01.head
        c_foot_01.tail += c_foot_01_vec
        c_foot_01.head += c_foot_01_vec
        c_foot_01.parent = toes_end
        # ~ set_bone_layer(c_foot_01, layer_ctrl_idx)
        set_bone_collection(rig, c_foot_01, coll_ctrl_name)
        c_foot_01["mixamo_ctrl"] = 1# tag as controller bone

        # Foot_ik_target parent
        foot_ik_target.parent = c_foot_01

        # Foot 01 Pole
        foot_01_pole_name = leg_rig_names["foot_01_pole"]+_side
        foot_01_pole = create_edit_bone(foot_01_pole_name)
        foot_01_pole.head = c_foot_01.head + (c_foot_01.z_axis * 0.05 * c_foot_01.length * 40)
        foot_01_pole.tail = foot_01_pole.head + (c_foot_01.z_axis * 0.05 * c_foot_01.length * 40)
        foot_01_pole.roll = radians(180)
        foot_01_pole.parent = c_foot_01
        # ~ set_bone_layer(foot_01_pole, layer_intern_idx)
        set_bone_collection(rig, foot_01_pole, coll_intern_name)

        # Toe IK Ctrl
        c_toe_ik_name = c_prefix+leg_rig_names["toes_ik"]+_side
        c_toe_ik = create_edit_bone(c_toe_ik_name)
        copy_bone_transforms(toe, c_toe_ik)
        c_toe_ik.parent = toes_end
        # ~ set_bone_layer(c_toe_ik, layer_ctrl_idx)
        set_bone_collection(rig, c_toe_ik, coll_ctrl_name)
        c_toe_ik["mixamo_ctrl"] = 1# tag as controller bone

        # Toe Track
        toe_track_name = leg_rig_names["toes_track"]+_side
        toe_track = create_edit_bone(toe_track_name)
        copy_bone_transforms(toe, toe_track)
        toe_track.parent = foot_ik
        # ~ set_bone_layer(toe_track, layer_intern_idx)
        set_bone_collection(rig, toe_track, coll_intern_name)

        # Toe_01 IK
        toe_01_ik_name = leg_rig_names["toes_01_ik"]+_side
        toe_01_ik = create_edit_bone(toe_01_ik_name)
        copy_bone_transforms(toe, toe_01_ik)
        toe_01_ik.tail = toe_01_ik.head + (toe_01_ik.tail-toe_01_ik.head)*0.5
        toe_01_ik.parent = toe_track
        # ~ set_bone_layer(toe_01_ik, layer_intern_idx)
        set_bone_collection(rig, toe_01_ik, coll_intern_name)

        # Toe_02
        toe_02_name = leg_rig_names["toes_02"]+_side
        toe_02 = create_edit_bone(toe_02_name)
        copy_bone_transforms(toe, toe_02)
        toe_02.head = toe_02.head + (toe_02.tail-toe_02.head)*0.5
        toe_02.parent = toe_01_ik
        # ~ set_bone_layer(toe_02, layer_intern_idx)
        set_bone_collection(rig, toe_02, coll_intern_name)

        # Toe FK Ctrl
        c_toe_fk_name = c_prefix+leg_rig_names["toes_fk"]+_side
        c_toe_fk = create_edit_bone(c_toe_fk_name)
        copy_bone_transforms(toe, c_toe_fk)
        c_toe_fk.parent = foot_fk
        # ~ set_bone_layer(c_toe_fk, layer_ctrl_idx)
        set_bone_collection(rig, c_toe_fk, coll_ctrl_name)
        c_toe_fk["mixamo_ctrl"] = 1# tag as controller bone

        # Foot Roll Cursor Ctrl
        c_foot_roll_cursor_name = c_prefix+leg_rig_names["foot_roll_cursor"]+_side
        c_foot_roll_cursor = create_edit_bone(c_foot_roll_cursor_name)
        copy_bone_transforms(c_foot_ik, c_foot_roll_cursor)
        vec = c_foot_roll_cursor.tail - c_foot_roll_cursor.head
        dist = 1.2
        c_foot_roll_cursor.head -= vec*dist
        c_foot_roll_cursor.tail -= vec*dist
        c_foot_roll_cursor.parent = c_foot_ik
        # ~ set_bone_layer(c_foot_roll_cursor, layer_ctrl_idx)
        set_bone_collection(rig, c_foot_roll_cursor, coll_ctrl_name)
        c_foot_roll_cursor["mixamo_ctrl"] = 1# tag as controller bone

        # Pole IK Ctrl
        c_pole_ik_name = c_prefix+leg_rig_names["pole_ik"]+_side
        c_pole_ik = create_edit_bone(c_pole_ik_name)
        # ~ set_bone_layer(c_pole_ik, layer_ctrl_idx)
        set_bone_collection(rig, c_pole_ik, coll_ctrl_name)
        c_pole_ik["mixamo_ctrl"] = 1# tag as controller bone

        plane_normal = (thigh_ik.head - calf_ik.tail)
        prepole_dir = calf_ik.head - leg_midpoint
        pole_pos = calf_ik.head + prepole_dir.normalized()
        pole_pos = project_point_onto_plane(pole_pos, calf_ik.head, plane_normal)
        pole_pos = calf_ik.head + ((pole_pos - calf_ik.head).normalized() * (calf_ik.head - thigh.head).magnitude * 1.7)

        c_pole_ik.head = pole_pos
        c_pole_ik.tail = [c_pole_ik.head[0], c_pole_ik.head[1], c_pole_ik.head[2] + (0.165 * thigh_ik.length * 2)]

        ik_pole_angle = get_pole_angle(thigh_ik, calf_ik, c_pole_ik.head)


        # -- Pose --
        bpy.ops.object.mode_set(mode='POSE')

        # Add constraints to control/mechanic bones

        # Calf IK
        calf_ik_pb = get_pose_bone(calf_ik_name)

        cns_name = "IK"
        ik_cns = calf_ik_pb.constraints.get(cns_name)
        if ik_cns == None:
            ik_cns = calf_ik_pb.constraints.new("IK")
            ik_cns.name = cns_name
        ik_cns.target = rig
        ik_cns.subtarget = foot_ik_target_name
        ik_cns.pole_target = rig
        ik_cns.pole_subtarget = c_pole_ik_name
        ik_cns.pole_angle = ik_pole_angle
        ik_cns.chain_count = 2
        ik_cns.use_tail = True
        ik_cns.use_stretch = False

        calf_ik_pb.lock_ik_y = True
        calf_ik_pb.lock_ik_z = True


        # Foot IK
        foot_ik_pb = get_pose_bone(foot_ik_name)

        cns_name = "Copy Location"
        copy_loc_cns = foot_ik_pb.constraints.get(cns_name)
        if copy_loc_cns == None:
            copy_loc_cns = foot_ik_pb.constraints.new("COPY_LOCATION")
            copy_loc_cns.name = cns_name
        copy_loc_cns.target = rig
        copy_loc_cns.subtarget = calf_ik_name
        copy_loc_cns.head_tail = 1.0

        cns_name = "TrackTo"
        cns = foot_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = foot_ik_pb.constraints.new("TRACK_TO")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_foot_01_name
        cns.head_tail = 0.0
        cns.track_axis = "TRACK_Y"
        cns.up_axis = "UP_Z"
        cns.use_target_z = True

        cns_name = "Locked Track"
        cns = foot_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = foot_ik_pb.constraints.new("LOCKED_TRACK")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = foot_01_pole_name
        cns.head_tail = 0.0
        cns.track_axis = "TRACK_Z"
        cns.lock_axis = "LOCK_Y"

        cns_name = "Copy Scale"
        cns = foot_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = foot_ik_pb.constraints.new("COPY_SCALE")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_foot_ik_name

        # Foot Ctrl IK
        c_foot_ik_pb = get_pose_bone(c_foot_ik_name)

        cns_name = "Child Of"
        cns = c_foot_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = c_foot_ik_pb.constraints.new("CHILD_OF")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = "Ctrl_Master"


        # Pole IK
        c_pole_ik_pb = get_pose_bone(c_pole_ik_name)

        cns_name = "Child Of"
        child_cns = c_pole_ik_pb.constraints.get(cns_name)
        if child_cns == None:
            child_cns = c_pole_ik_pb.constraints.new("CHILD_OF")
            child_cns.name = cns_name
        child_cns.target = rig
        child_cns.subtarget = c_foot_ik_name

        cns_power = 8

        # Toe End
        toes_end_pb = get_pose_bone(toes_end_name)
        len = toes_end_pb.length * cns_power

        cns_name = "Transformation"
        cns = toes_end_pb.constraints.get(cns_name)
        if cns == None:
            cns = toes_end_pb.constraints.new("TRANSFORM")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_foot_roll_cursor_name
        cns.use_motion_extrapolate = True
        cns.target_space = cns.owner_space ="LOCAL"
        cns.map_from = "LOCATION"
        cns.from_min_z = 0.5 * len
        cns.from_max_z = -0.5 * len
        cns.map_to = "ROTATION"
        cns.map_to_x_from = "Z"
        cns.map_to_z_from = "X"
        cns.to_min_x_rot = -2.61
        cns.to_max_x_rot = 2.61
        cns.mix_mode_rot = "ADD"

        cns_name = "Limit Rotation"
        cns = toes_end_pb.constraints.get(cns_name)
        if cns == None:
            cns = toes_end_pb.constraints.new("LIMIT_ROTATION")
            cns.name = cns_name
        cns.owner_space ="LOCAL"
        cns.use_limit_x = True
        cns.min_x = -2*pi
        cns.max_x = 0.0

        # Toe 01 ik
        toe_01_ik_pb = get_pose_bone(toe_01_ik_name)

        cns_name = "Copy Transforms"
        cns = toe_01_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = toe_01_ik_pb.constraints.new("COPY_TRANSFORMS")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_toe_ik_name
        cns.mix_mode = "REPLACE"
        cns.target_space = cns.owner_space = "WORLD"

        # Toe 02
        toe_02_pb = get_pose_bone(toe_02_name)

        cns_name = "Copy CopyRotation"
        cns = toe_02_pb.constraints.get(cns_name)
        if cns == None:
            cns = toe_02_pb.constraints.new("COPY_ROTATION")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_toe_ik_name
        cns.mix_mode = "REPLACE"
        cns.target_space = cns.owner_space = "WORLD"

        # Toe Track
        toe_track_pb = get_pose_bone(toe_track_name)

        cns_name = "TrackTo"
        cns = toe_track_pb.constraints.get(cns_name)
        if cns == None:
            cns = toe_track_pb.constraints.new("TRACK_TO")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = toes_end_01_name
        cns.head_tail = 0.0
        cns.track_axis = 'TRACK_Y'
        cns.up_axis = "UP_Z"
        cns.use_target_z = True

        # Heel Mid
        heel_mid_pb = get_pose_bone(heel_mid_name)
        len = heel_mid_pb.length * cns_power

        cns_name = "Transformation"
        cns = heel_mid_pb.constraints.get(cns_name)
        if cns == None:
            cns = heel_mid_pb.constraints.new("TRANSFORM")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_foot_roll_cursor_name
        cns.owner_space = cns.target_space = "LOCAL"
        cns.map_from = "LOCATION"
        cns.from_min_z = -0.25 * len
        cns.from_max_z = 0.25 * len
        cns.map_to = "ROTATION"
        cns.map_to_x_from = "Z"
        cns.map_to_y_from = "X"
        cns.map_to_z_from = "Y"
        cns.to_min_x_rot = radians(100)
        cns.to_max_x_rot = -radians(100)
        cns.mix_mode_rot = 'ADD'

        cns_name = "Limit Rotation"
        cns = heel_mid_pb.constraints.get(cns_name)
        if cns == None:
            cns = heel_mid_pb.constraints.new("LIMIT_ROTATION")
            cns.name = cns_name
        cns.use_limit_x = True
        cns.min_x = radians(0)
        cns.max_x = radians(360)
        cns.owner_space = "LOCAL"

        # Heel In
        heel_in_pb = get_pose_bone(heel_in_name)
        len = heel_in_pb.length * cns_power

        cns_name = "Transformation"
        cns = heel_in_pb.constraints.get(cns_name)
        if cns == None:
            cns = heel_in_pb.constraints.new("TRANSFORM")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_foot_roll_cursor_name
        cns.owner_space = cns.target_space = "LOCAL"
        cns.map_from = "LOCATION"
        cns.from_min_x = -0.25 * len
        cns.from_max_x = 0.25 * len
        cns.map_to = "ROTATION"
        cns.map_to_x_from = "Z"
        cns.map_to_y_from = "X"
        cns.map_to_z_from = "Y"
        cns.to_min_y_rot = -radians(100)
        cns.to_max_y_rot = radians(100)
        cns.mix_mode_rot = 'ADD'

        cns_name = "Limit Rotation"
        cns = heel_in_pb.constraints.get(cns_name)
        if cns == None:
            cns = heel_in_pb.constraints.new("LIMIT_ROTATION")
            cns.name = cns_name
        cns.use_limit_y = True

        if side == "Left":
            cns.min_y = 0.0
            cns.max_y = radians(90)
        elif side == "Right":
            cns.min_y = radians(-90)
            cns.max_y = radians(0.0)

        cns.owner_space = "LOCAL"

        # Heel Out
        heel_out_pb = get_pose_bone(heel_out_name)
        len = heel_out_pb.length * cns_power

        cns_name = "Transformation"
        cns = heel_out_pb.constraints.get(cns_name)
        if cns == None:
            cns = heel_out_pb.constraints.new("TRANSFORM")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_foot_roll_cursor_name
        cns.owner_space = cns.target_space = "LOCAL"
        cns.map_from = "LOCATION"
        cns.from_min_x = -0.25 * len
        cns.from_max_x = 0.25 * len
        cns.map_to = "ROTATION"
        cns.map_to_x_from = "Z"
        cns.map_to_y_from = "X"
        cns.map_to_z_from = "Y"
        cns.to_min_y_rot = -radians(100)
        cns.to_max_y_rot = radians(100)
        cns.mix_mode_rot = 'ADD'

        cns_name = "Limit Rotation"
        cns = heel_out_pb.constraints.get(cns_name)
        if cns == None:
            cns = heel_out_pb.constraints.new("LIMIT_ROTATION")
            cns.name = cns_name
        cns.use_limit_y = True

        if side == "Left":
            cns.min_y = radians(-90)
            cns.max_y = radians(0.0)
        elif side == "Right":
            cns.min_y = radians(0.0)
            cns.max_y = radians(90)

        cns.owner_space = "LOCAL"


        # Add constraints to Mixamo bones
        foot_pb = get_pose_bone(foot_name)
        thigh_pb = get_pose_bone(thigh_name)

        # IK-FK switch property
        if not "ik_fk_switch" in c_foot_ik_pb.keys():
            create_custom_prop(node=c_foot_ik_pb, prop_name="ik_fk_switch", prop_val=0.0, prop_min=0.0, prop_max=1.0, prop_description="IK-FK switch value")

        c_foot_ik_pb["ik_fk_switch"] = 0.0 if self.ik_legs else 1.0

        # Thigh
        cns_name = "IK_follow"
        cns_ik = thigh_pb.constraints.get(cns_name)
        if cns_ik == None:
            cns_ik = thigh_pb.constraints.new("COPY_TRANSFORMS")
            cns_ik.name = cns_name
        cns_ik.target = rig
        cns_ik.subtarget = thigh_ik_name
        cns_ik.influence = 1.0

        cns_name = "FK_follow"
        cns_fk = thigh_pb.constraints.get(cns_name)
        if cns_fk == None:
            cns_fk = thigh_pb.constraints.new("COPY_TRANSFORMS")
            cns_fk.name = cns_name
        cns_fk.target = rig
        cns_fk.subtarget = c_thigh_fk_name
        cns_fk.influence = 0.0

        add_driver_to_prop(rig, 'pose.bones["'+thigh_name+'"].constraints["'+cns_name+'"].influence', 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")

        # Calf
        calf_pb = get_pose_bone(calf_name)

        cns_name = "IK_follow"
        cns_ik = calf_pb.constraints.get(cns_name)
        if cns_ik == None:
            cns_ik = calf_pb.constraints.new("COPY_TRANSFORMS")
            cns_ik.name = cns_name
        cns_ik.target = rig
        cns_ik.subtarget = calf_ik_name
        cns_ik.influence = 1.0

        cns_name = "FK_follow"
        cns_fk = calf_pb.constraints.get(cns_name)
        if cns_fk == None:
            cns_fk = calf_pb.constraints.new("COPY_TRANSFORMS")
            cns_fk.name = cns_name
        cns_fk.target = rig
        cns_fk.subtarget = c_calf_fk_name
        cns_fk.influence = 0.0

        add_driver_to_prop(rig, 'pose.bones["'+calf_name+'"].constraints["'+cns_name+'"].influence', 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")

        # Foot
        cns_name = "IK_follow"
        cns_ik = foot_pb.constraints.get(cns_name)
        if cns_ik == None:
            cns_ik = foot_pb.constraints.new("COPY_TRANSFORMS")
            cns_ik.name = cns_name
        cns_ik.target = rig
        cns_ik.subtarget = foot_ik_name
        cns_ik.influence = 1.0

        cns_name = "FK_follow"
        cns_fk = foot_pb.constraints.get(cns_name)
        if cns_fk == None:
            cns_fk = foot_pb.constraints.new("COPY_TRANSFORMS")
            cns_fk.name = cns_name
        cns_fk.target = rig
        cns_fk.subtarget = foot_fk_name
        cns_fk.influence = 0.0

        add_driver_to_prop(rig, 'pose.bones["'+foot_name+'"].constraints["'+cns_name+'"].influence', 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")


        # Toe
        toe_pb = get_pose_bone(toe_name)

        cns_name = "IK_Rot_follow"
        cns_ik_rot = toe_pb.constraints.get(cns_name)
        if cns_ik_rot == None:
            cns_ik_rot = toe_pb.constraints.new("COPY_ROTATION")
            cns_ik_rot.name = cns_name
        cns_ik_rot.target = rig
        cns_ik_rot.subtarget = c_toe_ik_name
        cns_ik_rot.influence = 1.0

        cns_name = "IK_Scale_follow"
        cns_ik_scale = toe_pb.constraints.get(cns_name)
        if cns_ik_scale == None:
            cns_ik_scale = toe_pb.constraints.new("COPY_SCALE")
            cns_ik_scale.name = cns_name
        cns_ik_scale.target = rig
        cns_ik_scale.subtarget = c_toe_ik_name
        cns_ik_scale.influence = 1.0

        cns_name_fk_rot = "FK_Rot_follow"
        cns_fk_rot = toe_pb.constraints.get(cns_name_fk_rot)
        if cns_fk_rot == None:
            cns_fk_rot = toe_pb.constraints.new("COPY_ROTATION")
            cns_fk_rot.name = cns_name_fk_rot
        cns_fk_rot.target = rig
        cns_fk_rot.subtarget = c_toe_fk_name
        cns_fk_rot.influence = 1.0

        cns_name_fk_scale = "FK_Scale_follow"
        cns_fk_scale = toe_pb.constraints.get(cns_name_fk_scale)
        if cns_fk_scale == None:
            cns_fk_scale = toe_pb.constraints.new("COPY_SCALE")
            cns_fk_scale.name = cns_name_fk_scale
        cns_fk_scale.target = rig
        cns_fk_scale.subtarget = c_toe_fk_name
        cns_fk_scale.influence = 1.0

        add_driver_to_prop(rig, 'pose.bones["'+toe_name+'"].constraints["'+cns_name_fk_rot+'"].influence', 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")
        add_driver_to_prop(rig, 'pose.bones["'+toe_name+'"].constraints["'+cns_name_fk_scale+'"].influence', 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")


        c_foot_01_pb = get_pose_bone(c_foot_01_name)
        c_foot_roll_cursor_pb = get_pose_bone(c_foot_roll_cursor_name)
        c_thigh_fk_pb = get_pose_bone(c_thigh_fk_name)
        c_calf_fk_pb = get_pose_bone(c_calf_fk_name)
        c_foot_fk_pb = get_pose_bone(c_foot_fk_name)
        c_toe_ik_pb = get_pose_bone(c_toe_ik_name)
        c_toe_fk_pb = get_pose_bone(c_toe_fk_name)


        # Set transforms locks
        lock_pbone_transform(c_foot_roll_cursor_pb, "location", [1])
        lock_pbone_transform(c_foot_roll_cursor_pb, "rotation", [0,1,2])
        lock_pbone_transform(c_foot_roll_cursor_pb, "scale", [0,1,2])

        lock_pbone_transform(c_foot_01_pb, "location", [0,1,2])
        lock_pbone_transform(c_foot_01_pb, "rotation", [1,2])
        lock_pbone_transform(c_foot_01_pb, "scale", [0,1,2])

        lock_pbone_transform(c_foot_fk_pb, "location", [0,1,2])

        lock_pbone_transform(c_pole_ik_pb, "rotation", [0,1,2])
        lock_pbone_transform(c_pole_ik_pb, "scale", [0,1,2])

        lock_pbone_transform(c_thigh_fk_pb, "location", [0,1,2])
        lock_pbone_transform(c_calf_fk_pb, "location", [0,1,2])


        c_pbones_list = [c_foot_ik_pb, c_pole_ik_pb, c_foot_01_pb, c_foot_roll_cursor_pb, c_thigh_fk_pb, c_calf_fk_pb, c_foot_fk_pb, c_toe_fk_pb, c_toe_ik_pb]

        # Set custom shapes
        set_bone_custom_shape(c_thigh_fk_pb, "cs_thigh_fk")
        set_bone_custom_shape(c_calf_fk_pb, "cs_calf_fk")
        set_bone_custom_shape(c_foot_ik_pb, "cs_foot")
        set_bone_custom_shape(c_foot_fk_pb, "cs_foot")
        set_bone_custom_shape(c_pole_ik_pb, "cs_sphere_012")
        set_bone_custom_shape(c_foot_roll_cursor_pb, "cs_foot_roll")
        set_bone_custom_shape(c_foot_01_pb, "cs_foot_01")
        set_bone_custom_shape(c_toe_fk_pb, "cs_toe")
        set_bone_custom_shape(c_toe_ik_pb, "cs_toe")

        # set custom shape drivers
        ik_controls_names = [c_foot_ik_name, c_foot_01_name, c_toe_ik_name, c_foot_roll_cursor_name, c_pole_ik_name]

        arr_ids = [-1]
        if blender_version._float >= 300:
            arr_ids = [0, 1, 2]

        for n in ik_controls_names:
            dr_dp = 'pose.bones["'+n+'"].'+get_custom_shape_scale_prop_name()
            tar_dp = 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]'
            for arr_id in arr_ids:
                add_driver_to_prop(rig, dr_dp, tar_dp, array_idx=arr_id, exp="1-var")

        fk_controls_names = [c_foot_fk_name, c_thigh_fk_name, c_calf_fk_name, c_toe_fk_name]

        for n in fk_controls_names:
            dr_dp = 'pose.bones["'+n+'"].'+get_custom_shape_scale_prop_name()
            tar_dp = 'pose.bones["'+c_foot_ik_name+'"]["ik_fk_switch"]'
            for arr_id in arr_ids:
                add_driver_to_prop(rig, dr_dp, tar_dp, array_idx=arr_id, exp="var")


        for pb in c_pbones_list:
            # set rotation euler
            pb.rotation_mode = "XYZ"
            # set color group
            set_bone_color_group(rig, pb, "body"+_side.lower())


    def add_arm(side):
        print("  Add Arm", side)
        _side = "_"+side
        shoulder_name = get_mix_name(side+arm_names["shoulder"], use_name_prefix)
        arm_name = get_mix_name(side+arm_names["arm"], use_name_prefix)
        forearm_name = get_mix_name(side+arm_names["forearm"], use_name_prefix)
        hand_name = get_mix_name(side+arm_names["hand"], use_name_prefix)


        # -- Edit --
        bpy.ops.object.mode_set(mode='EDIT')


        shoulder = get_edit_bone(shoulder_name)
        arm = get_edit_bone(arm_name)
        forearm = get_edit_bone(forearm_name)
        hand = get_edit_bone(hand_name)


        if not shoulder or not arm or not forearm or not hand:
            print("    Arm bones are missing, skip arm: "+side)
            return


        # Create bones
        # Fingers
        fingers_names = []
        c_fingers_names = []
        fingers = []
        finger_leaves = []

        for fname in fingers_type:
            for i in range(1, 4):
                finger_name = get_mix_name(side+"Hand"+fname+str(i), use_name_prefix)
                finger = get_edit_bone(finger_name)
                if finger == None:
                    continue

                fingers_names.append(finger_name)
                fingers.append(finger)
                c_finger_name = c_prefix+fname+str(i)+_side
                c_fingers_names.append(c_finger_name)
                c_finger = create_edit_bone(c_finger_name)
                copy_bone_transforms(finger, c_finger)
                # ~ set_bone_layer(c_finger, 0)
                set_bone_collection(rig, c_finger, coll_ctrl_name)
                c_finger["mixamo_ctrl"] = 1# tag as controller bone

                if i == 1:
                    c_finger.parent = hand
                else:
                    prev_finger_name = c_prefix+fname+str(i-1)+_side
                    prev_finger = get_edit_bone(prev_finger_name)
                    c_finger.parent = prev_finger

        # fingers "leaves"/tip bones
        for fname in fingers_type:
            finger_name = get_mix_name(side+"Hand"+fname+"4", use_name_prefix)
            finger_leaf = get_edit_bone(finger_name)
            finger_leaves.append(finger_leaf)

        # Set Mixamo bones in layer
        for b in [shoulder, arm, forearm, hand] + fingers + finger_leaves:
            # ~ set_bone_layer(b, layer_mix_idx)
            set_bone_collection(rig, b, coll_mix_name)

        # Shoulder Ctrl
        c_shoulder_name = c_prefix+arm_rig_names["shoulder"]+_side
        c_shoulder = create_edit_bone(c_shoulder_name)
        copy_bone_transforms(shoulder, c_shoulder)
        c_shoulder.parent = get_edit_bone(c_prefix+spine_rig_names["spine3"])
        # ~ set_bone_layer(c_shoulder, layer_ctrl_idx)
        set_bone_collection(rig, c_shoulder, coll_ctrl_name)
        c_shoulder["mixamo_ctrl"] = 1# tag as controller bone

        # Arm IK
        arm_ik_name = arm_rig_names["arm_ik"]+_side
        arm_ik = create_edit_bone(arm_ik_name)
        copy_bone_transforms(arm, arm_ik)

        # correct straight arms angle, need minimum 0.1 degrees for IK constraints to work
        angle_min = 0.1

        def get_arm_angle():
            #return degrees(arm.y_axis.angle(forearm.y_axis))
            vec1 = forearm.head - arm.head
            vec2 = hand.head - forearm.head
            return degrees(vec1.angle(vec2))

        arm_angle = get_arm_angle()

        if arm_angle < angle_min:
            print("    ! Straight arm bones, angle = "+str(arm_angle))

            max_iter = 10000
            i = 0

            while arm_angle < angle_min and i < max_iter:

                dir = ((arm.x_axis + forearm.x_axis)*0.5).normalized()
                if side == "Right":
                    dir *= -1

                forearm.head += dir * (forearm.tail-forearm.head).magnitude * 0.0001
                arm_angle = get_arm_angle()
                i += 1

            print("      corrected arm angle: "+str(arm_angle))

        # auto-align knee position with global Y axis to ensure IK pole vector is physically correct
        arm_axis = forearm.tail - arm.head
        arm_midpoint = (arm.head + forearm.tail) * 0.5
        #cur_vec = forearm.head - arm_midpoint
        #cur_vec[0] = 0.0
        #global_y_vec = Vector((0, 1, 0))

        dir = forearm.head - arm_midpoint
        cur_vec = project_vector_onto_plane(dir, arm_axis)
        global_y_vec = project_vector_onto_plane(Vector((0, 1, 0)), arm_axis)
        signed_cur_angle = signed_angle(cur_vec, global_y_vec, arm_axis)
        print("    IK correc angle:", degrees(signed_cur_angle))

        # rotate
        rotated_point = rotate_point(forearm.head.copy(), -signed_cur_angle, arm_midpoint, arm_axis)
        """
        rot_mat = Matrix.Rotation(-signed_cur_angle, 4, arm_axis.normalized())
            # rotate in world origin space
        offset_vec = -arm_midpoint
        offset_elbow = forearm.head + offset_vec
            # rotate
        rotated_point = rot_mat @ offset_elbow
            # bring back to original space
        rotated_point = rotated_point -offset_vec
        """

        # (check)
        dir = rotated_point - arm_midpoint
        cur_vec = project_vector_onto_plane(dir, arm_axis)
        signed_cur_angle = signed_angle(cur_vec, global_y_vec, arm_axis)
        print("    IK corrected angle:", degrees(signed_cur_angle))

        arm_ik.tail = rotated_point

        arm_ik.parent = c_shoulder
        # ~ set_bone_layer(arm_ik, layer_intern_idx)
        set_bone_collection(rig, arm_ik, coll_intern_name)

        # Arm FK Ctrl
        c_arm_fk_name = c_prefix+arm_rig_names["arm_fk"]+_side
        c_arm_fk = create_edit_bone(c_arm_fk_name)
        c_arm_fk.parent = get_edit_bone(c_prefix+spine_rig_names["spine3"])
        copy_bone_transforms(arm_ik, c_arm_fk)
        # ~ set_bone_layer(c_arm_fk, layer_ctrl_idx)
        set_bone_collection(rig, c_arm_fk, coll_ctrl_name)
        c_arm_fk["mixamo_ctrl"] = 1# tag as controller bone


        # ForeArm IK
        forearm_ik_name = arm_rig_names["forearm_ik"]+_side
        forearm_ik = create_edit_bone(forearm_ik_name)
        copy_bone_transforms(forearm, forearm_ik)
        forearm_ik.head = arm_ik.tail.copy()
        forearm_ik.tail = hand.head.copy()
        forearm_ik.parent = arm_ik
        # ~ set_bone_layer(forearm_ik, layer_intern_idx)
        set_bone_collection(rig, forearm_ik, coll_intern_name)

        # align arm and forearm IK roll
            # align forearm_ik local Z
        align_bone_x_axis(forearm_ik, (forearm_ik.head-arm_midpoint))
            # align arm_ik on forearm_ik
        align_bone_x_axis(arm_ik, forearm_ik.x_axis)
            # copy arm_ik to c_arm_fk
        copy_bone_transforms(arm_ik, c_arm_fk)

        if side == "Right":
            forearm_ik.roll += radians(180)
            arm_ik.roll += radians(180)
            c_arm_fk.roll += radians(180)

        # Forearm FK Ctrl
        c_forearm_fk_name = c_prefix+arm_rig_names["forearm_fk"]+_side
        c_forearm_fk = create_edit_bone(c_forearm_fk_name)
        copy_bone_transforms(forearm_ik, c_forearm_fk)
        c_forearm_fk.parent = c_arm_fk
        # ~ set_bone_layer(c_forearm_fk, layer_ctrl_idx)
        set_bone_collection(rig, c_forearm_fk, coll_ctrl_name)
        c_forearm_fk["mixamo_ctrl"] = 1# tag as controller bone


        # Pole IK Ctrl
        c_pole_ik_name = c_prefix+arm_rig_names["pole_ik"]+_side
        c_pole_ik = create_edit_bone(c_pole_ik_name)
        # ~ set_bone_layer(c_pole_ik, layer_ctrl_idx)
        set_bone_collection(rig, c_pole_ik, coll_ctrl_name)
        c_pole_ik["mixamo_ctrl"] = 1# tag as controller bone

        arm_midpoint = (arm_ik.head + forearm_ik.tail) * 0.5

        plane_normal = (arm_ik.head - forearm_ik.tail)
        prepole_dir = forearm_ik.head - arm_midpoint
        pole_pos = forearm_ik.head + prepole_dir.normalized()
        pole_pos = project_point_onto_plane(pole_pos, forearm_ik.head, plane_normal)
        pole_pos = forearm_ik.head + ((pole_pos - forearm_ik.head).normalized() * (forearm_ik.head - arm.head).magnitude * 1.0)

        c_pole_ik.head = pole_pos
        c_pole_ik.tail = [c_pole_ik.head[0], c_pole_ik.head[1], c_pole_ik.head[2] + (0.165 * arm_ik.length * 4)]

        ik_pole_angle = get_pole_angle(arm_ik, forearm_ik, c_pole_ik.head)

        # Hand IK Ctrl
        c_hand_ik_name = c_prefix + arm_rig_names["hand_ik"]+_side
        c_hand_ik = create_edit_bone(c_hand_ik_name)
        # ~ set_bone_layer(c_hand_ik, layer_ctrl_idx)
        set_bone_collection(rig, c_hand_ik, coll_ctrl_name)
        copy_bone_transforms(hand, c_hand_ik)
        c_hand_ik["mixamo_ctrl"] = 1# tag as controller bone

        # Hand FK Ctrl
        c_hand_fk_name = c_prefix+arm_rig_names["hand_fk"]+_side
        c_hand_fk = create_edit_bone(c_hand_fk_name)
        copy_bone_transforms(hand, c_hand_fk)
        c_hand_fk.parent = c_forearm_fk
        # ~ set_bone_layer(c_hand_fk, layer_ctrl_idx)
        set_bone_collection(rig, c_hand_fk, coll_ctrl_name)
        c_hand_fk["mixamo_ctrl"] = 1# tag as controller bone

        # ---- Pose ----
        bpy.ops.object.mode_set(mode='POSE')


        # Add constraints to control/mechanic bones
        c_shoulder_pb = get_pose_bone(c_shoulder_name)
        shoulder_pb = get_pose_bone(shoulder_name)
        c_arm_fk_pb = get_pose_bone(c_arm_fk_name)
        forearm_ik_pb = get_pose_bone(forearm_ik_name)
        c_pole_ik_pb = get_pose_bone(c_pole_ik_name)
        c_hand_ik_pb = get_pose_bone(c_hand_ik_name)


        # Arm FK Ctrl
        cns_name = "Copy Location"
        cns = c_arm_fk_pb.constraints.get(cns_name)
        if cns == None:
            cns = c_arm_fk_pb.constraints.new("COPY_LOCATION")
            cns.name = cns_name
        cns.head_tail = 1.0
        cns.target = rig
        cns.subtarget = c_shoulder_name

        # Forearm IK
        cns_name = "IK"
        ik_cns = forearm_ik_pb.constraints.get(cns_name)
        if ik_cns == None:
            ik_cns = forearm_ik_pb.constraints.new("IK")
            ik_cns.name = cns_name
        ik_cns.target = rig
        ik_cns.subtarget = c_hand_ik_name
        ik_cns.pole_target = rig
        ik_cns.pole_subtarget = c_pole_ik_name
        ik_cns.pole_angle = 0.0
        if side == "Right":
            ik_cns.pole_angle = radians(180)
        ik_cns.chain_count = 2
        ik_cns.use_tail = True
        ik_cns.use_stretch = False

        forearm_ik_pb.lock_ik_y = True
        forearm_ik_pb.lock_ik_x = True


        # Pole IK Ctrl
        cns_name = "Child Of"
        cns = c_pole_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = c_pole_ik_pb.constraints.new("CHILD_OF")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_prefix+spine_rig_names["pelvis"]


        # Hand IK Ctrl
        cns_name = "Child Of"
        cns = c_hand_ik_pb.constraints.get(cns_name)
        if cns == None:
            cns = c_hand_ik_pb.constraints.new("CHILD_OF")
            cns.name = cns_name
        cns.target = rig
        cns.subtarget = c_master_name


        # Add constraints to Mixamo bones
        hand_pb = get_pose_bone(hand_name)

            # Fingers
        for i, fname in enumerate(c_fingers_names):
            c_finger_pb = get_pose_bone(fname)
            finger_pb = get_pose_bone(fingers_names[i])
            add_copy_transf(finger_pb, rig, c_finger_pb.name)


            # Shoulder
        add_copy_transf(shoulder_pb, rig, c_shoulder_pb.name)


       # IK-FK switch property
        if not "ik_fk_switch" in c_hand_ik_pb.keys():
            create_custom_prop(node=c_hand_ik_pb, prop_name="ik_fk_switch", prop_val=0.0, prop_min=0.0, prop_max=1.0, prop_description="IK-FK switch value")

        c_hand_ik_pb["ik_fk_switch"] = 0.0 if self.ik_arms else 1.0


            # Arm
        arm_pb = get_pose_bone(arm_name)

        cns_ik_name = "IK_follow"
        cns_ik = arm_pb.constraints.get(cns_ik_name)
        if cns_ik == None:
            cns_ik = arm_pb.constraints.new("COPY_TRANSFORMS")
            cns_ik.name = cns_ik_name
        cns_ik.target = rig
        cns_ik.subtarget = arm_ik_name
        cns_ik.influence = 1.0

        cns_fk_name = "FK_Follow"
        cns_fk = arm_pb.constraints.get(cns_fk_name)
        if cns_fk == None:
            cns_fk = arm_pb.constraints.new("COPY_TRANSFORMS")
            cns_fk.name = cns_fk_name
        cns_fk.target = rig
        cns_fk.subtarget = c_arm_fk_name
        cns_fk.influence = 0.0

        add_driver_to_prop(rig, 'pose.bones["'+arm_name+'"].constraints["'+cns_fk_name+'"].influence', 'pose.bones["'+c_hand_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")


        # ForeArm
        forearm_pb = get_pose_bone(forearm_name)

        cns_ik_name = "IK_follow"
        cns_ik = forearm_pb.constraints.get(cns_ik_name)
        if cns_ik == None:
            cns_ik = forearm_pb.constraints.new("COPY_TRANSFORMS")
            cns_ik.name = cns_ik_name
        cns_ik.target = rig
        cns_ik.subtarget = forearm_ik_name
        cns_ik.influence = 1.0

        cns_fk_name = "FK_Follow"
        cns_fk = forearm_pb.constraints.get(cns_fk_name)
        if cns_fk == None:
            cns_fk = forearm_pb.constraints.new("COPY_TRANSFORMS")
            cns_fk.name = cns_fk_name
        cns_fk.target = rig
        cns_fk.subtarget = c_forearm_fk_name
        cns_fk.influence = 0.0

        add_driver_to_prop(rig, 'pose.bones["'+forearm_name+'"].constraints["'+cns_fk_name+'"].influence', 'pose.bones["'+c_hand_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")

        c_arm_fk_pb = get_pose_bone(c_arm_fk_name)
        c_forearm_fk_pb = get_pose_bone(c_forearm_fk_name)

        lock_pbone_transform(c_forearm_fk_pb, "location", [0,1,2])

        # Hand
        cns_ik_name = "IK_follow"
        cns_ik = hand_pb.constraints.get(cns_ik_name)
        if cns_ik == None:
            cns_ik = hand_pb.constraints.new("COPY_ROTATION")
            cns_ik.name = cns_ik_name
        cns_ik.target = rig
        cns_ik.subtarget = c_hand_ik_name
        cns_ik.influence = 1.0

        cns_fk_name = "FK_Follow"
        cns_fk = hand_pb.constraints.get(cns_fk_name)
        if cns_fk == None:
            cns_fk = hand_pb.constraints.new("COPY_ROTATION")
            cns_fk.name = cns_fk_name
        cns_fk.target = rig
        cns_fk.subtarget = c_hand_fk_name
        cns_fk.influence = 0.0

        add_driver_to_prop(rig, 'pose.bones["'+hand_name+'"].constraints["'+cns_fk_name+'"].influence', 'pose.bones["'+c_hand_ik_name+'"]["ik_fk_switch"]', array_idx=-1, exp="var")

        c_hand_fk_pb = get_pose_bone(c_hand_fk_name)
        lock_pbone_transform(c_hand_fk_pb, "location", [0,1,2])

        # Set custom shapes
        c_hand_ik_pb = get_pose_bone(c_hand_ik_name)
        set_bone_custom_shape(c_shoulder_pb, "cs_shoulder_"+side.lower())
        set_bone_custom_shape(c_arm_fk_pb, "cs_arm_fk")
        set_bone_custom_shape(c_forearm_fk_pb, "cs_forearm_fk")
        set_bone_custom_shape(c_pole_ik_pb, "cs_sphere_012")
        set_bone_custom_shape(c_hand_fk_pb, "cs_hand")
        set_bone_custom_shape(c_hand_ik_pb, "cs_hand")

        c_fingers_pb = []

        for fname in c_fingers_names:
            finger_pb = get_pose_bone(fname)
            c_fingers_pb.append(finger_pb)
            set_bone_custom_shape(finger_pb, "cs_circle_025")

        c_pbones_list = [c_shoulder_pb, c_arm_fk_pb, c_forearm_fk_pb, c_pole_ik_pb, c_hand_fk_pb, c_hand_ik_pb] + c_fingers_pb


        # set custom shape drivers
        ik_controls_names = [c_pole_ik_name, c_hand_ik_name]

        arr_ids = [-1]
        if blender_version._float >= 300:
            arr_ids = [0, 1, 2]

        for n in ik_controls_names:
            dr_dp = 'pose.bones["'+n+'"].'+get_custom_shape_scale_prop_name()
            tar_dp = 'pose.bones["'+c_hand_ik_name+'"]["ik_fk_switch"]'
            for arr_id in arr_ids:
                add_driver_to_prop(rig, dr_dp, tar_dp, array_idx=arr_id, exp="1-var")

        fk_controls_names = [c_arm_fk_name, c_forearm_fk_name, c_hand_fk_name]

        for n in fk_controls_names:
            dr_dp = 'pose.bones["'+n+'"].'+get_custom_shape_scale_prop_name()
            tar_dp = 'pose.bones["'+c_hand_ik_name+'"]["ik_fk_switch"]'
            for arr_id in arr_ids:
                add_driver_to_prop(rig, dr_dp, tar_dp, array_idx=arr_id, exp="var")


        for pb in c_pbones_list:
            # set rotation euler
            pb.rotation_mode = "XYZ"
            # set color group
            set_bone_color_group(rig, pb, "body"+_side.lower())


    add_master()
    add_spine()
    add_head()
    add_arm("Left")
    add_arm("Right")
    add_leg("Left")
    add_leg("Right")

    # tag the armature with a custom prop to specify the control rig is built
    rig.data["mr_control_rig"] = True


def _zero_out():
    print("\nZeroing out...")
    scn = bpy.context.scene
    arm = bpy.data.objects.get(bpy.context.active_object.name)

    print("  Clear anim")
    # Clear animation data
    action = None
    if arm.animation_data:
        if arm.animation_data.action:
            action = arm.animation_data.action

    if action:
        while len(action.fcurves):
            action.fcurves.remove(action.fcurves[0])

    print("  Clear pose")
    # Reset pose
    bpy.ops.object.mode_set(mode='POSE')

    for b in arm.pose.bones:
        b.location = [0,0,0]
        b.rotation_euler = [0,0,0]
        b.rotation_quaternion = [1,0,0,0]
        b.scale = [1,1,1]

    print("Zeroed out.")


def _bake_anim(self):
    scn = bpy.context.scene

    # get min-max frame range
    rig = bpy.context.active_object

    if rig.animation_data is None:
        print("No animation data, exit bake")
        return

    if rig.animation_data.nla_tracks is None:
        print("No NLA tracks found, exit bake")
        return

    tracks = rig.animation_data.nla_tracks

    fs = None
    fe = None

    # from NLA tracks
    for track in tracks:
        for strip in track.strips:
            if fs is None:
                fs = strip.frame_start
            if fe is None:
                fe = strip.frame_end

            if strip.frame_start < fs:
                fs = strip.frame_start
            if strip.frame_end > fe:
                fe = strip.frame_end

    if fs is None or fe is None:
        print("No NLA tracks found, exit")
        return

    # get active action frame range
    act = rig.animation_data.action
    if act is not None:
        if act.frame_range[0] < fs:
            fs = act.frame_range[0]
        if act.frame_range[1] > fe:
            fe = act.frame_range[1]

    # select only controllers bones
    bpy.ops.object.mode_set(mode='POSE')
    bpy.ops.pose.select_all(action='DESELECT')

    found_ctrl = False
    for pbone in rig.pose.bones:
        if "mixamo_ctrl" in pbone.bone.keys():
            rig.data.bones.active = pbone.bone
            pbone.bone.select = True
            found_ctrl = True

    if not found_ctrl:# backward compatibility, use layer 0 instead
        print("Ctrl bones not tagged, search in layer 0 instead...")
        c0 = rig.data.collections.get("CTRL")
        if c0 is not None:
            for b in c0.bones:
                pb = rig.pose.bones.get(b.name)
                if pb is not None:
                    rig.data.bones.active = pb.bone
                    pb.bone.select = True

        # ~ for pbone in rig.pose.bones:
            # ~ if pbone.bone.layers[0]:
                # ~ rig.data.bones.active = pbone.bone
                # ~ pbone.bone.select = True

    fs, fe = int(fs), int(fe)

    scn.frame_set(fs)
    bpy.context.view_layer.update()

    # bake NLA strips
    print("Baking, frame start:", fs, ",frame end", fe)
    bpy.ops.nla.bake(frame_start=fs, frame_end=fe, step=1, only_selected=True, visual_keying=False,
                     clear_constraints=False, clear_parents=False, use_current_action=False,
                     clean_curves=False, bake_types={'POSE'})

    # remove tracks
    while len(tracks):
        rig.animation_data.nla_tracks.remove(tracks[0])


def redefine_source_rest_pose(src_arm, tar_arm):
    print("  Redefining source rest pose...")

    scn = bpy.context.scene

    src_arm_loc = src_arm.location.copy()
    src_arm.location = [0,0,0]
    fr_range = src_arm.animation_data.action.frame_range
    fr_start = int(fr_range[0])
    fr_end = int(fr_range[1])

    # duplicate source armature
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(src_arm.name)
    bpy.ops.object.mode_set(mode='OBJECT')
    duplicate_object()
    src_arm_dupli = get_object(bpy.context.active_object.name)
    src_arm_dupli["mix_to_del"] = True


    """
    # Store bone matrices
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(src_arm.name)
    bpy.ops.object.mode_set(mode='POSE')

    bones_data = []

    for f in range(fr_start, fr_end+1):
        print("Frame", f)
        scn.frame_set(f)
        bpy.context.view_layer.update()

        bones_matrices = {}

        for pbone in src_arm.pose.bones:
            bones_matrices[pbone.name] = pbone.matrix.copy()
            #bones_matrices[pbone.name] = src_arm.convert_space(pose_bone=pbone, matrix=pbone.matrix, from_space="POSE", to_space="LOCAL")


        bones_data.append((f, bones_matrices))
    """

    # Store target bones rest transforms
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(tar_arm.name)
    bpy.ops.object.mode_set(mode='EDIT')

    rest_bones = {}

    for ebone in tar_arm.data.edit_bones:
        rest_bones[ebone.name] = ebone.head.copy(), ebone.tail.copy(), vec_roll_to_mat3(ebone.y_axis, ebone.roll)

    # Apply source bones rest transforms
    print("  Set rest pose...")
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(src_arm.name)
    bpy.ops.object.mode_set(mode='EDIT')

    for bname in rest_bones:
        ebone = get_edit_bone(bname)

        if ebone == None:
            #print("Warning, bone not found on source armature:", bname)
            continue

        head, tail, mat3 = rest_bones[bname]
        ebone.head, ebone.tail, ebone.roll = src_arm.matrix_world.inverted() @ head, src_arm.matrix_world.inverted() @ tail, mat3_to_vec_roll(src_arm.matrix_world.inverted().to_3x3() @ mat3)


    # Add constraints
    bpy.ops.object.mode_set(mode='POSE')

    for pb in src_arm.pose.bones:
        cns = pb.constraints.new("COPY_TRANSFORMS")
        cns.name = "temp"
        cns.target = src_arm_dupli
        cns.subtarget = pb.name

    # Restore animation
    print("Restore animation...")
    bake_anim(frame_start=fr_start, frame_end=fr_end, only_selected=False, bake_bones=True, bake_object=False)

    # Restore location
    src_arm.location = src_arm_loc

    # Delete temp data
        # constraints
    for pb in src_arm.pose.bones:
        if len(pb.constraints):
            cns = pb.constraints.get("temp")
            if cns:
                pb.constraints.remove(cns)

        # src_arm_dupli
    delete_object(src_arm_dupli)

    print("  Source armature rest pose redefined.")


def _import_anim(src_arm, tar_arm, import_only=False):
    print("\nImporting animation...")
    scn = bpy.context.scene

    if src_arm.animation_data == None:
        print("  No action found on the source armature")
        return

    if src_arm.animation_data.action == None:
        print("  No action found on the source armature")
        return

    if len(src_arm.animation_data.action.fcurves) == 0:
        print("  No keyframes to import")
        return

    use_name_prefix = True

    # Redefine source armature rest pose if importing only animation, since
    # Mixamo Fbx may have different rest pose when the Fbx file contains only animation data
    if bpy.context.view_layer.objects.active == None:
        set_active_object(tar_arm.name)
    if import_only:
        redefine_source_rest_pose(src_arm, tar_arm)

    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(tar_arm.name)
    bpy.ops.object.mode_set(mode='POSE')

    hand_left_name = get_mix_name("LeftHand", use_name_prefix)
    hand_right_name = get_mix_name("RightHand", use_name_prefix)
    foot_left_name = get_mix_name("LeftFoot", use_name_prefix)
    foot_right_name = get_mix_name("RightFoot", use_name_prefix)

    hand_left_pb = get_pose_bone(hand_left_name)
    c_hand_ik_left_pb = get_pose_bone(c_prefix + arm_rig_names["hand_ik"]+"_Left")
    hand_right_pb = get_pose_bone(hand_right_name)
    c_hand_ik_right_pb = get_pose_bone(c_prefix + arm_rig_names["hand_ik"]+"_Right")
    foot_left_pb = get_pose_bone(foot_left_name)
    c_foot_ik_left_pb = get_pose_bone(c_prefix + leg_rig_names["foot_ik"]+"_Left")
    foot_right_pb = get_pose_bone(foot_right_name)
    c_foot_ik_right_pb = get_pose_bone(c_prefix + leg_rig_names["foot_ik"]+"_Right")

    arm_left_kinematic = "IK" if c_hand_ik_left_pb["ik_fk_switch"] < 0.5 else "FK"
    arm_right_kinematic = "IK" if c_hand_ik_right_pb["ik_fk_switch"] < 0.5 else "FK"
    leg_left_kinematic = "IK" if c_foot_ik_left_pb["ik_fk_switch"] < 0.5 else "FK"
    leg_right_kinematic = "IK" if c_foot_ik_right_pb["ik_fk_switch"] < 0.5 else "FK"


    # Set bones mapping for retargetting
    bones_map = {}

    bones_map[get_mix_name("Hips", use_name_prefix)] = c_prefix+"Hips"
    bones_map[get_mix_name("Spine", use_name_prefix)] = c_prefix+"Spine"
    bones_map[get_mix_name("Spine1", use_name_prefix)] = c_prefix+"Spine1"
    bones_map[get_mix_name("Spine2", use_name_prefix)] = c_prefix+"Spine2"
    bones_map[get_mix_name("Neck", use_name_prefix)] = c_prefix+"Neck"
    bones_map[get_mix_name("Head", use_name_prefix)] = c_prefix+"Head"
    bones_map[get_mix_name("LeftShoulder", use_name_prefix)] = c_prefix+"Shoulder_Left"
    bones_map[get_mix_name("RightShoulder", use_name_prefix)] = c_prefix+"Shoulder_Right"

    # Arm
    if arm_left_kinematic == "FK":
        bones_map[get_mix_name("LeftArm", use_name_prefix)] = c_prefix+"Arm_FK_Left"
        bones_map[get_mix_name("LeftForeArm", use_name_prefix)] = c_prefix+"ForeArm_FK_Left"
        bones_map[get_mix_name("LeftHand", use_name_prefix)] = c_prefix+"Hand_FK_Left"
    elif arm_left_kinematic == "IK":
        bones_map[c_prefix+"Hand_IK_Left"] = c_prefix+"Hand_IK_Left"

    if arm_right_kinematic == "FK":
        bones_map[get_mix_name("RightArm", use_name_prefix)] = c_prefix+"Arm_FK_Right"
        bones_map[get_mix_name("RightForeArm", use_name_prefix)] = c_prefix+"ForeArm_FK_Right"
        bones_map[get_mix_name("RightHand", use_name_prefix)] = c_prefix+"Hand_FK_Right"
    elif arm_right_kinematic == "IK":
        bones_map[c_prefix+"Hand_IK_Right"] = c_prefix+"Hand_IK_Right"

    # Fingers
    bones_map[get_mix_name("LeftHandThumb1", use_name_prefix)] = c_prefix+"Thumb1_Left"
    bones_map[get_mix_name("LeftHandThumb2", use_name_prefix)] = c_prefix+"Thumb2_Left"
    bones_map[get_mix_name("LeftHandThumb3", use_name_prefix)] = c_prefix+"Thumb3_Left"
    bones_map[get_mix_name("LeftHandIndex1", use_name_prefix)] = c_prefix+"Index1_Left"
    bones_map[get_mix_name("LeftHandIndex2", use_name_prefix)] = c_prefix+"Index2_Left"
    bones_map[get_mix_name("LeftHandIndex3", use_name_prefix)] = c_prefix+"Index3_Left"
    bones_map[get_mix_name("LeftHandMiddle1", use_name_prefix)] = c_prefix+"Middle1_Left"
    bones_map[get_mix_name("LeftHandMiddle2", use_name_prefix)] = c_prefix+"Middle2_Left"
    bones_map[get_mix_name("LeftHandMiddle3", use_name_prefix)] = c_prefix+"Middle3_Left"
    bones_map[get_mix_name("LeftHandRing1", use_name_prefix)] = c_prefix+"Ring1_Left"
    bones_map[get_mix_name("LeftHandRing2", use_name_prefix)] = c_prefix+"Ring2_Left"
    bones_map[get_mix_name("LeftHandRing3", use_name_prefix)] = c_prefix+"Ring3_Left"
    bones_map[get_mix_name("LeftHandPinky1", use_name_prefix)] = c_prefix+"Pinky1_Left"
    bones_map[get_mix_name("LeftHandPinky2", use_name_prefix)] = c_prefix+"Pinky2_Left"
    bones_map[get_mix_name("LeftHandPinky3", use_name_prefix)] = c_prefix+"Pinky3_Left"
    bones_map[get_mix_name("RightHandThumb1", use_name_prefix)] = c_prefix+"Thumb1_Right"
    bones_map[get_mix_name("RightHandThumb2", use_name_prefix)] = c_prefix+"Thumb2_Right"
    bones_map[get_mix_name("RightHandThumb3", use_name_prefix)] = c_prefix+"Thumb3_Right"
    bones_map[get_mix_name("RightHandIndex1", use_name_prefix)] = c_prefix+"Index1_Right"
    bones_map[get_mix_name("RightHandIndex2", use_name_prefix)] = c_prefix+"Index2_Right"
    bones_map[get_mix_name("RightHandIndex3", use_name_prefix)] = c_prefix+"Index3_Right"
    bones_map[get_mix_name("RightHandMiddle1", use_name_prefix)] = c_prefix+"Middle1_Right"
    bones_map[get_mix_name("RightHandMiddle2", use_name_prefix)] = c_prefix+"Middle2_Right"
    bones_map[get_mix_name("RightHandMiddle3", use_name_prefix)] = c_prefix+"Middle3_Right"
    bones_map[get_mix_name("RightHandRing1", use_name_prefix)] = c_prefix+"Ring1_Right"
    bones_map[get_mix_name("RightHandRing2", use_name_prefix)] = c_prefix+"Ring2_Right"
    bones_map[get_mix_name("RightHandRing3", use_name_prefix)] = c_prefix+"Ring3_Right"
    bones_map[get_mix_name("RightHandPinky1", use_name_prefix)] = c_prefix+"Pinky1_Right"
    bones_map[get_mix_name("RightHandPinky2", use_name_prefix)] = c_prefix+"Pinky2_Right"
    bones_map[get_mix_name("RightHandPinky3", use_name_prefix)] = c_prefix+"Pinky3_Right"
    bones_map["Root"] = "Ctrl_Master"
#    bones_map["Root"] = "Root"

    if leg_left_kinematic == "FK":
        bones_map[get_mix_name("LeftUpLeg", use_name_prefix)] = c_prefix+"UpLeg_FK_Left"
        bones_map[get_mix_name("LeftLeg", use_name_prefix)] = c_prefix+"Leg_FK_Left"
        bones_map[c_prefix+"Foot_FK_Left"] = c_prefix+"Foot_FK_Left"
        bones_map[get_mix_name("LeftToeBase", use_name_prefix)] = c_prefix+"Toe_FK_Left"
    elif leg_left_kinematic == "IK":
        bones_map[c_prefix+"Foot_IK_Left"] = c_prefix+"Foot_IK_Left"
        bones_map[get_mix_name("LeftToeBase", use_name_prefix)] = c_prefix+"Toe_IK_Left"

    if leg_right_kinematic == "FK":
        bones_map[get_mix_name("RightUpLeg", use_name_prefix)] = c_prefix+"UpLeg_FK_Right"
        bones_map[get_mix_name("RightLeg", use_name_prefix)] = c_prefix+"Leg_FK_Right"
        bones_map[c_prefix+"Foot_FK_Right"] = c_prefix+"Foot_FK_Right"
        bones_map[get_mix_name("RightToeBase", use_name_prefix)] = c_prefix+"Toe_FK_Right"
    elif leg_right_kinematic == "IK":
        bones_map[c_prefix+"Foot_IK_Right"] = c_prefix+"Foot_IK_Right"
        bones_map[get_mix_name("RightToeBase", use_name_prefix)] = c_prefix+"Toe_IK_Right"



    action = None
    if src_arm.animation_data == None:
        print("  No action found on the source armature")
    if src_arm.animation_data.action == None:
        print("  No action found on the source armature")

    # Work on a source armature duplicate
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(src_arm.name)

    duplicate_object()
    src_arm_copy_name = src_arm.name+"_COPY"
    bpy.context.active_object.name = src_arm_copy_name
    src_arm = get_object(src_arm_copy_name)
    src_arm["mix_to_del"] = True

    # Get anim data
    action = src_arm.animation_data.action
    fr_start = int(action.frame_range[0])
    fr_end = int(action.frame_range[1])


    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    set_active_object(tar_arm.name)

    # Store bones data from target armature
    bpy.ops.object.mode_set(mode='EDIT')

    ctrl_matrices = {}
    ik_bones_data = {}

    kinematics = {"HandLeft":["Hand", arm_left_kinematic,"Left"], "HandRight":["Hand", arm_right_kinematic, "Right"], "FootLeft":["Foot", leg_left_kinematic, "Left"], "FootRight":["Foot", leg_right_kinematic, "Right"]}
    for b in kinematics:
        type, kin_mode, side = kinematics[b]
        ctrl_name = c_prefix+type+'_'+kin_mode+'_'+side
        ctrl_ebone = get_edit_bone(ctrl_name)
        mix_bone_name = get_mix_name(side+type, use_name_prefix)

        ctrl_matrices[ctrl_name] = ctrl_ebone.matrix.copy(), mix_bone_name

        # store corrected ik bones
        if kin_mode == "IK":
            ik_bones = {}
            ik_chain = []

            if type == "Foot":
                ik_chain = ["UpLeg_IK_"+side, "Leg_IK_"+side]
            elif type == "Hand":
                ik_chain = ["Arm_IK_"+side, "ForeArm_IK_"+side]

            ik1 = get_edit_bone(ik_chain[0])
            ik2 = get_edit_bone(ik_chain[1])

            ik_bones["ik1"] = ik1.name, ik1.head.copy(), ik1.tail.copy(), ik1.roll
            ik_bones["ik2"] = ik2.name, ik2.head.copy(), ik2.tail.copy(), ik2.roll
            ik_bones_data[b] = type, side, ik_bones


    # Init source armature rotation and scale
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')

    set_active_object(src_arm.name)

    scale_fac = src_arm.scale[0]
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    for fc in action.fcurves:
        dp = fc.data_path
        if dp.startswith('pose.bones') and dp.endswith(".location"):
            for k in fc.keyframe_points:
                k.co[1] *= scale_fac


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

    # Add helper source bones
        # add feet bones helpers
    for name in ctrl_matrices:
        foot_ebone = create_edit_bone(name)
        foot_ebone.head, foot_ebone.tail = [0,0,0], [0,0,0.1]
        foot_ebone.matrix = ctrl_matrices[name][0]
        foot_ebone.parent = get_edit_bone(ctrl_matrices[name][1])

        # add IK bones helpers
    for b in ik_bones_data:
        type, side, ik_bones = ik_bones_data[b]
        for bone_type in ik_bones:
            bname, bhead, btail, broll = ik_bones[bone_type]
            ebone = create_edit_bone(bname)
            ebone.head, ebone.tail, ebone.roll = bhead, btail, broll

        # set parents
    for b in ik_bones_data:
        type, side, ik_bones = ik_bones_data[b]
        ik2_name = ik_bones["ik2"][0]
        ik2 = get_edit_bone(ik2_name)

        # set constraints
    bpy.ops.object.mode_set(mode='POSE')

    bake_ik_data = {"src_arm":src_arm}

    for b in ik_bones_data:
        type, side, ik_bones = ik_bones_data[b]
        b1_name = ik_bones["ik1"][0]
        b2_name = ik_bones["ik2"][0]
        b1_pb = get_pose_bone(b1_name)
        b2_pb = get_pose_bone(b2_name)

        chain = []
        if type == "Foot":
            chain = [get_mix_name(side+"UpLeg", use_name_prefix), get_mix_name(side+"Leg", use_name_prefix)]
            bake_ik_data["Leg"+side] = chain

        elif type == "Hand":
            chain = [get_mix_name(side+"Arm", use_name_prefix), get_mix_name(side+"ForeArm", use_name_prefix)]
            bake_ik_data["Arm"+side] = chain

        cns = b1_pb.constraints.new("COPY_TRANSFORMS")
        cns.name = "Copy Transforms"
        cns.target = src_arm
        cns.subtarget = chain[0]

        cns = b2_pb.constraints.new("COPY_TRANSFORMS")
        cns.name = "Copy Transforms"
        cns.target = src_arm
        cns.subtarget = chain[1]

    # Retarget
    retarget_method = 2

    # Method 1: Direct matrix retargetting (slower)
    if retarget_method == 1:
        for fr in range(fr_start, fr_end+1):
            print("  frame", fr)
            scn.frame_set(fr)
            bpy.context.view_layer.update()

            for src_name in bones_map:
                tar_name = bones_map[src_name]
                src_bone = src_arm.pose.bones.get(src_name)
                tar_bone = tar_arm.pose.bones.get(tar_name)

                if "Foot" in src_name:
                    tar_mix_bone = tar_arm.pose.bones.get(src_name)
                    #print("  tar_mix_bone", tar_mix_bone.name)
                    offset_mat = tar_bone.matrix @ tar_mix_bone.matrix.inverted()
                    tar_bone.matrix = offset_mat @ src_bone.matrix.copy()
                else:
                    tar_bone.matrix = src_bone.matrix.copy()

                if not "Hips" in src_name:
                    tar_bone.location = [0,0,0]

                bpy.context.view_layer.update()# Not ideal, slow performances


    # Method 2: Constrained retargetting (faster)
    elif retarget_method == 2:
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        set_active_object(tar_arm.name)
        bpy.ops.object.mode_set(mode='POSE')
        bpy.ops.pose.select_all(action='DESELECT')


        # add constraints
        for src_name in bones_map:
            tar_name = bones_map[src_name]
            src_bone = src_arm.pose.bones.get(src_name)
            tar_bone = tar_arm.pose.bones.get(tar_name)

            if src_bone == None:
                #print("SKIP BONE", src_name)
                continue
            if tar_bone == None:
                #print("SKIP BONE", tar_name)
                continue
            if "Root" in src_name:
                print("CONSTRAINT", src_bone, tar_bone)

            cns_name = "Copy Rotation_retarget"
            cns = tar_bone.constraints.new('COPY_ROTATION')
            cns.name = cns_name
            cns.target = src_arm
            cns.subtarget = src_name

            if "Hips" in src_name:
                cns_name = "Copy Location_retarget"
                cns = tar_bone.constraints.new('COPY_LOCATION')
                cns.name = cns_name
                cns.target = src_arm
                cns.subtarget = src_name
                cns.owner_space = cns.target_space = "LOCAL"

            # Foot IK, Hand IK
            if (leg_left_kinematic == "IK" and "Foot_IK_Left" in src_name) or (leg_right_kinematic == "IK" and "Foot_IK_Right" in src_name) or (arm_left_kinematic == "IK" and "Hand_IK_Left" in src_name) or (arm_right_kinematic == "IK" and "Hand_IK_Right" in src_name):
                #print("  set IK remap constraints", src_name)
                cns_name = "Copy Location_retarget"
                cns = tar_bone.constraints.new('COPY_LOCATION')
                cns.name = cns_name
                cns.target = src_arm
                cns.subtarget = src_name
                cns.target_space = cns.owner_space = "POSE"

                # select IK poles
                _side = "_Left" if "Left" in src_name else "_Right"
                ik_pole_name = ""
                if "Hand" in src_name:
                    ik_pole_name = c_prefix+arm_rig_names["pole_ik"]+_side
                elif "Foot" in src_name:
                    ik_pole_name = c_prefix+leg_rig_names["pole_ik"]+_side

                ik_pole_ctrl = get_pose_bone(ik_pole_name)
                tar_arm.data.bones.active = ik_pole_ctrl.bone
                ik_pole_ctrl.bone.select = True


            # select
            tar_arm.data.bones.active = tar_bone.bone
            tar_bone.bone.select = True

        bpy.context.view_layer.update()

        # bake
        bake_anim(frame_start=fr_start, frame_end=fr_end, only_selected=True, bake_bones=True, bake_object=False, ik_data=bake_ik_data)

    bpy.ops.object.mode_set(mode='OBJECT')
    set_active_object(src_arm.name)
    set_active_object(tar_arm.name)
    print("Animation imported.")


def remove_retarget_cns(armature):
    #print("Removing constraints...")
    for pb in armature.pose.bones:
        if len(pb.constraints):
            for cns in pb.constraints:
                if cns.name.endswith("_retarget") or cns.name == "temp":
                    pb.constraints.remove(cns)


def remove_temp_objects():
    for obj in bpy.data.objects:
        if "mix_to_del" in obj.keys():
            delete_object(obj)


def update_mixamo_tab():
    try:
        bpy.utils.unregister_class(MR_PT_MenuMain)
        bpy.utils.unregister_class(MR_PT_MenuRig)
        bpy.utils.unregister_class(MR_PT_MenuAnim)
        bpy.utils.unregister_class(MR_PT_MenuExport)
        bpy.utils.unregister_class(MR_PT_MenuUpdate)
    except:
        pass

    MixamoRigPanel.bl_category = bpy.context.preferences.addons[__package__].preferences.mixamo_tab_name
    bpy.utils.register_class(MR_PT_MenuMain)
    bpy.utils.register_class(MR_PT_MenuRig)
    bpy.utils.register_class(MR_PT_MenuAnim)
    bpy.utils.register_class(MR_PT_MenuExport)
    bpy.utils.register_class(MR_PT_MenuUpdate)

###########  UI PANELS  ###################
class MixamoRigPanel:
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Mixamo"


class MR_PT_MenuMain(Panel, MixamoRigPanel):
    bl_label = "Mixamo Control Rig"

    def draw(self, context):
        scn = context.scene

        layt = self.layout
        layt.use_property_split = True
        layt.use_property_decorate = False

        #col = layt.column(align=True)
        #col.scale_y = 1.3
        #col.prop_search(scn, "mix_source_armature", scn, "objects", text="Skeleton")
        arm_name = "None"

        if context.active_object != None:
            if context.active_object.type == "ARMATURE":
                arm_name = context.active_object.name

        layt.label(text="Character: "+arm_name)


class MR_PT_MenuRig(Panel, MixamoRigPanel):
    bl_label = "Control Rig"
    bl_parent_id = "MR_PT_MenuMain"

    def draw(self, context):
        layt = self.layout
        layt.use_property_split = True
        layt.use_property_decorate = False

        obj = context.active_object
        scn = context.scene

        """
        has_rigged = False
        if obj:
            if obj.type == "ARMATURE":
                if len(obj.data.keys()):
                    if "mr_data" in obj.data.keys():
                        has_rigged = True
        """

        col = layt.column(align=True)
        col.scale_y = 1.3

        col.operator(MR_OT_make_rig.bl_idname, text="Create Control Rig")
        col.operator(MR_OT_zero_out.bl_idname, text="Zero Out Rig")

        col = layt.column(align=True)
        col.separator()

        if bpy.context.mode != 'EDIT_MESH':
            col.operator(MR_OT_edit_custom_shape.bl_idname, text="Edit Control Shape")
        else:
            col.operator(MR_OT_apply_shape.bl_idname, text="Apply Control Shape")


class MR_PT_MenuAnim(Panel, MixamoRigPanel):
    bl_label = "Animation"
    bl_parent_id = "MR_PT_MenuMain"

    def draw(self, context):
        layt = self.layout
        layt.use_property_split = True
        layt.use_property_decorate = False# No animation.
        scn = context.scene
        layt.use_property_split = True
        layt.use_property_decorate = False

        col = layt.column(align=True)
        col.scale_y = 1
        #col.prop_search(scn, "mix_target_armature", scn, "objects", text="Control Rig")
        col.label(text="Source Skeleton:")
        col.prop_search(scn, "mix_source_armature", scn, "objects", text="")
        col.separator()

        col = layt.column(align=True)
        col.scale_y = 1.3
        col.operator(MR_OT_import_anim.bl_idname, text="Apply Animation to Control Rig")

        col = layt.column(align=True)
        col.scale_y = 1.3
        col.operator(MR_OT_bake_anim.bl_idname, text="Bake Animation")


class MR_PT_MenuUpdate(Panel, MixamoRigPanel):
    bl_label = "Update"
    bl_parent_id = "MR_PT_MenuMain"

    def draw(self, context):
        layt = self.layout
        layt.operator(MR_OT_update.bl_idname, text="Update Control Rig")


class MR_PT_MenuExport(Panel, MixamoRigPanel):
    bl_label = "Export"
    bl_parent_id = "MR_PT_MenuMain"

    def draw(self, context):
        layt = self.layout
        layt.operator('export_scene.gltf', text="GLTF Export...")#MR_OT_exportGLTF.bl_idname


###########  REGISTER  ##################
classes = (
    MR_PT_MenuMain,
    MR_PT_MenuRig,
    MR_PT_MenuAnim,
    MR_PT_MenuExport,
    MR_PT_MenuUpdate,
    MR_OT_make_rig,
    MR_OT_zero_out,
    MR_OT_bake_anim,
    MR_OT_import_anim,
    MR_OT_edit_custom_shape,
    MR_OT_apply_shape,
    MR_OT_exportGLTF,
    MR_OT_update
    )


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

    update_mixamo_tab()

    bpy.types.Scene.mix_source_armature = bpy.props.PointerProperty(type=bpy.types.Object)
    bpy.types.Scene.mix_target_armature = bpy.props.PointerProperty(type=bpy.types.Object)


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

    del bpy.types.Scene.mix_source_armature
    del bpy.types.Scene.mix_target_armature

if __name__ == "__main__":
    register()
