# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

import bpy
from mathutils import Vector
from bpy_extras import anim_utils
from .  import retarget


### Utility Functions


def getConsObj(bone):
    #utility function - returns related IK target if bone has IK
    ik = [constraint for constraint in bone.constraints if constraint.type == "IK"]
    if ik:
        ik = ik[0]
        cons_obj = ik.target
        if ik.subtarget:
            cons_obj = ik.target.pose.bones[ik.subtarget]
    else:
        cons_obj = bone
    return cons_obj


def consObjToBone(cons_obj):
    #Utility function - returns related bone from ik object
    if cons_obj.name[-3:] == "Org":
        return cons_obj.name[:-3]
    else:
        return cons_obj.name

### And and Remove Constraints (called from operators)


def addNewConstraint(m_constraint, cons_obj):
     #Decide the correct Blender constraint according to the Mocap constraint type
    if m_constraint.type == "point" or m_constraint.type == "freeze":
        c_type = "LIMIT_LOCATION"
    if m_constraint.type == "distance":
        c_type = "LIMIT_DISTANCE"
    if m_constraint.type == "floor":
        c_type = "LIMIT_LOCATION"
        #create and store the new constraint within m_constraint
    real_constraint = cons_obj.constraints.new(c_type)
    real_constraint.name = "Auto fixes " + str(len(cons_obj.constraints))
    m_constraint.real_constraint_bone = consObjToBone(cons_obj)
    m_constraint.real_constraint = real_constraint.name
    #set the rest of the constraint properties
    setConstraint(m_constraint, bpy.context)


def removeConstraint(m_constraint, cons_obj):
    #remove the influence fcurve and Blender constraint
    oldConstraint = cons_obj.constraints[m_constraint.real_constraint]
    removeFcurves(cons_obj, bpy.context.active_object, oldConstraint, m_constraint)
    cons_obj.constraints.remove(oldConstraint)

### Update functions. There are 3: UpdateType/Bone
### update framing (deals with changes in the desired frame range)
### And setConstraint which deals with the rest


def updateConstraintBoneType(m_constraint, context):
    #If the constraint exists, we need to remove it
    #from the old bone
    obj = context.active_object
    bones = obj.pose.bones
    if m_constraint.real_constraint:
        bone = bones[m_constraint.real_constraint_bone]
        cons_obj = getConsObj(bone)
        removeConstraint(m_constraint, cons_obj)
    #Regardless, after that we create a new constraint
    if m_constraint.constrained_bone:
        bone = bones[m_constraint.constrained_bone]
        cons_obj = getConsObj(bone)
        addNewConstraint(m_constraint, cons_obj)


def setConstraintFraming(m_constraint, context):
    obj = context.active_object
    bones = obj.pose.bones
    bone = bones[m_constraint.constrained_bone]
    cons_obj = getConsObj(bone)
    real_constraint = cons_obj.constraints[m_constraint.real_constraint]
    #remove the old keyframes
    removeFcurves(cons_obj, obj, real_constraint, m_constraint)
    #set the new ones according to the m_constraint properties
    s, e = m_constraint.s_frame, m_constraint.e_frame
    s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out
    real_constraint.influence = 1
    real_constraint.keyframe_insert(data_path="influence", frame=s)
    real_constraint.keyframe_insert(data_path="influence", frame=e)
    real_constraint.influence = 0
    real_constraint.keyframe_insert(data_path="influence", frame=s - s_in)
    real_constraint.keyframe_insert(data_path="influence", frame=e + s_out)


def removeFcurves(cons_obj, obj, real_constraint, m_constraint):
    #Determine if the constrained object is a bone or an empty
    if isinstance(cons_obj, bpy.types.PoseBone):
        fcurves = obj.animation_data.action.fcurves
    else:
        fcurves = cons_obj.animation_data.action.fcurves
    #Find the RNA data path of the constraint's influence
    RNA_paths = []
    RNA_paths.append(real_constraint.path_from_id("influence"))
    if m_constraint.type == "floor" or m_constraint.type == "point":
        RNA_paths += [real_constraint.path_from_id("max_x"), real_constraint.path_from_id("min_x")]
        RNA_paths += [real_constraint.path_from_id("max_y"), real_constraint.path_from_id("min_y")]
        RNA_paths += [real_constraint.path_from_id("max_z"), real_constraint.path_from_id("min_z")]
    #Retrieve the correct fcurve via the RNA data path and remove it
    fcurves_del = [fcurve for fcurve in fcurves if fcurve.data_path in RNA_paths]
    #clear the fcurve and set the frames.
    if fcurves_del:
        for fcurve in fcurves_del:
            fcurves.remove(fcurve)
    #remove armature fcurves (if user keyframed m_constraint properties)
    if obj.data.animation_data and m_constraint.type == "point":
        if obj.data.animation_data.action:
            path = m_constraint.path_from_id("targetPoint")
            m_fcurves = [fcurve for fcurve in obj.data.animation_data.action.fcurves if fcurve.data_path == path]
            for curve in m_fcurves:
                obj.data.animation_data.action.fcurves.remove(curve)

#Utility function for copying property fcurves over


def copyFCurve(newCurve, oldCurve):
    for point in oldCurve.keyframe_points:
        newCurve.keyframe_points.insert(frame=point.co.x, value=point.co.y)

#Creates new fcurves for the constraint properties (for floor and point)


def createConstraintFCurves(cons_obj, obj, real_constraint):
    if isinstance(cons_obj, bpy.types.PoseBone):
        c_fcurves = obj.animation_data.action.fcurves
    else:
        c_fcurves = cons_obj.animation_data.action.fcurves
    c_x_path = [real_constraint.path_from_id("max_x"), real_constraint.path_from_id("min_x")]
    c_y_path = [real_constraint.path_from_id("max_y"), real_constraint.path_from_id("min_y")]
    c_z_path = [real_constraint.path_from_id("max_z"), real_constraint.path_from_id("min_z")]
    c_constraints_path = c_x_path + c_y_path + c_z_path
    existing_curves = [fcurve for fcurve in c_fcurves if fcurve.data_path in c_constraints_path]
    if existing_curves:
        for curve in existing_curves:
            c_fcurves.remove(curve)
    xCurves, yCurves, zCurves = [], [], []
    for path in c_constraints_path:
        newCurve = c_fcurves.new(path)
        if path in c_x_path:
            xCurves.append(newCurve)
        elif path in c_y_path:
            yCurves.append(newCurve)
        else:
            zCurves.append(newCurve)
    return xCurves, yCurves, zCurves


# Function that copies all settings from m_constraint to the real Blender constraints
# Is only called when blender constraint already exists


def setConstraint(m_constraint, context):
    if not m_constraint.constrained_bone:
        return
    obj = context.active_object
    bones = obj.pose.bones
    bone = bones[m_constraint.constrained_bone]
    cons_obj = getConsObj(bone)
    real_constraint = cons_obj.constraints[m_constraint.real_constraint]
    NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap]
    obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track]

    #frame changing section
    setConstraintFraming(m_constraint, context)
    s, e = m_constraint.s_frame, m_constraint.e_frame
    s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out
    s -= s_in
    e += s_out
    #Set the blender constraint parameters
    if m_constraint.type == "point":
        constraint_settings = False  # are fix settings keyframed?
        if not m_constraint.targetSpace == "constrained_boneB":
            real_constraint.owner_space = m_constraint.targetSpace
        else:
            real_constraint.owner_space = "LOCAL"
        if obj.data.animation_data:
            if obj.data.animation_data.action:
                path = m_constraint.path_from_id("targetPoint")
                m_fcurves = [fcurve for fcurve in obj.data.animation_data.action.fcurves if fcurve.data_path == path]
                if m_fcurves:
                    constraint_settings = True
                    xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint)
                    for curve in xCurves:
                        copyFCurve(curve, m_fcurves[0])
                    for curve in yCurves:
                        copyFCurve(curve, m_fcurves[1])
                    for curve in zCurves:
                        copyFCurve(curve, m_fcurves[2])
        if m_constraint.targetSpace == "constrained_boneB" and m_constraint.constrained_boneB:
            c_frame = context.scene.frame_current
            bakedPos = {}
            src_bone = bones[m_constraint.constrained_boneB]
            if not constraint_settings:
                xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint)
            print("please wait a moment, calculating fix")
            for t in range(s, e):
                context.scene.frame_set(t)
                src_bone_pos = src_bone.matrix.to_translation()
                bakedPos[t] = src_bone_pos + m_constraint.targetPoint  # final position for constrained bone in object space
            context.scene.frame_set(c_frame)
            for frame in bakedPos.keys():
                pos = bakedPos[frame]
                for xCurve in xCurves:
                    xCurve.keyframe_points.insert(frame=frame, value=pos.x)
                for yCurve in yCurves:
                    yCurve.keyframe_points.insert(frame=frame, value=pos.y)
                for zCurve in zCurves:
                    zCurve.keyframe_points.insert(frame=frame, value=pos.z)

        if not constraint_settings:
            x, y, z = m_constraint.targetPoint
            real_constraint.max_x = x
            real_constraint.max_y = y
            real_constraint.max_z = z
            real_constraint.min_x = x
            real_constraint.min_y = y
            real_constraint.min_z = z
            real_constraint.use_max_x = True
            real_constraint.use_max_y = True
            real_constraint.use_max_z = True
            real_constraint.use_min_x = True
            real_constraint.use_min_y = True
            real_constraint.use_min_z = True

    if m_constraint.type == "freeze":
        context.scene.frame_set(s)
        real_constraint.owner_space = m_constraint.targetSpace
        bpy.context.scene.frame_set(m_constraint.s_frame)
        if isinstance(cons_obj, bpy.types.PoseBone):
            vec = obj.matrix_world * (cons_obj.matrix.to_translation())
            #~ if obj.parent:
                #~ vec = obj.parent.matrix_world * vec
            x, y, z = vec
        else:
            x, y, z = cons_obj.matrix_world.to_translation()

        real_constraint.max_x = x
        real_constraint.max_y = y
        real_constraint.max_z = z
        real_constraint.min_x = x
        real_constraint.min_y = y
        real_constraint.min_z = z
        real_constraint.use_max_x = True
        real_constraint.use_max_y = True
        real_constraint.use_max_z = True
        real_constraint.use_min_x = True
        real_constraint.use_min_y = True
        real_constraint.use_min_z = True

    if m_constraint.type == "distance" and m_constraint.constrained_boneB:
        real_constraint.owner_space = "WORLD"
        real_constraint.target = obj
        real_constraint.subtarget = getConsObj(bones[m_constraint.constrained_boneB]).name
        real_constraint.limit_mode = "LIMITDIST_ONSURFACE"
        if m_constraint.targetDist < 0.01:
            m_constraint.targetDist = 0.01
        real_constraint.distance = m_constraint.targetDist

    if m_constraint.type == "floor" and m_constraint.targetMesh:
        real_constraint.mute = True
        real_constraint.owner_space = "WORLD"
        #calculate the positions throughout the range
        s, e = m_constraint.s_frame, m_constraint.e_frame
        s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out
        s -= s_in
        e += s_out
        bakedPos = {}
        floor = bpy.data.objects[m_constraint.targetMesh]
        c_frame = context.scene.frame_current
        print("please wait a moment, calculating fix")
        for t in range(s, e):
            context.scene.frame_set(t)
            axis = obj.matrix_world.to_3x3() * Vector((0, 0, 1))
            offset = obj.matrix_world.to_3x3() * Vector((0, 0, m_constraint.targetDist))
            ray_origin = (cons_obj.matrix * obj.matrix_world).to_translation() - offset  # world position of constrained bone
            ray_target = ray_origin + axis
            #convert ray points to floor's object space
            ray_origin = floor.matrix_world.inverted() * ray_origin
            ray_target = floor.matrix_world.inverted() * ray_target
            ray_direction = ray_target - ray_origin
            ok, hit, nor, ind = floor.ray_cast(ray_origin, ray_direction)
            if ok:
                bakedPos[t] = (floor.matrix_world * hit)
                bakedPos[t] += Vector((0, 0, m_constraint.targetDist))
            else:
                bakedPos[t] = (cons_obj.matrix * obj.matrix_world).to_translation()
        context.scene.frame_set(c_frame)
        #create keyframes for real constraint
        xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint)
        for frame in bakedPos.keys():
            pos = bakedPos[frame]
            for xCurve in xCurves:
                xCurve.keyframe_points.insert(frame=frame, value=pos.x)
            for yCurve in yCurves:
                yCurve.keyframe_points.insert(frame=frame, value=pos.y)
            for zCurve in zCurves:
                zCurve.keyframe_points.insert(frame=frame, value=pos.z)
        real_constraint.use_max_x = True
        real_constraint.use_max_y = True
        real_constraint.use_max_z = True
        real_constraint.use_min_x = True
        real_constraint.use_min_y = True
        real_constraint.use_min_z = True

    # active/baked check
    real_constraint.mute = (not m_constraint.active)


def locBake(s_frame, e_frame, bones):
    scene = bpy.context.scene
    bakeDict = {}
    for bone in bones:
        bakeDict[bone.name] = {}
    for t in range(s_frame, e_frame):
        scene.frame_set(t)
        for bone in bones:
            bakeDict[bone.name][t] = bone.matrix.copy()
    for t in range(s_frame, e_frame):
        for bone in bones:
            print(bone.bone.matrix_local.to_translation())
            bone.matrix = bakeDict[bone.name][t]
            bone.keyframe_insert("location", frame=t)


# Baking function which bakes all bones effected by the constraint
def bakeAllConstraints(obj, s_frame, e_frame, bones):
    for bone in bones:
        bone.bone.select = False
    selectedBones = []  # Marks bones that need a full bake
    simpleBake = []  # Marks bones that need only a location bake
    for end_bone in bones:
        if end_bone.name in [m_constraint.real_constraint_bone for m_constraint in obj.data.mocap_constraints]:
            #For all bones that have a constraint:
            ik = retarget.hasIKConstraint(end_bone)
            cons_obj = getConsObj(end_bone)
            if ik:
                #If it's an auto generated IK:
                if ik.chain_count == 0:
                    selectedBones += bones  # Chain len 0, bake everything
                else:
                    selectedBones += [end_bone] + end_bone.parent_recursive[:ik.chain_count - 1]  # Bake the chain
            else:
                #It's either an FK bone which we should just bake
                #OR a user created IK target bone
                simpleBake += [end_bone]
    for bone in selectedBones:
        bone.bone.select = True
    NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap]
    obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track]
    constraintTrack = obj.animation_data.nla_tracks[NLATracks.auto_fix_track]
    constraintStrip = constraintTrack.strips[0]
    constraintStrip.action_frame_start = s_frame
    constraintStrip.action_frame_end = e_frame
    constraintStrip.frame_start = s_frame
    constraintStrip.frame_end = e_frame
    if selectedBones:
        # Use bake function from NLA Bake Action operator
        anim_utils.bake_action(
            obj,
            s_frame,
            e_frame,
            action=constraintStrip.action,
            only_selected=True,
            do_pose=True,
            do_object=False,
        )
    if simpleBake:
        #Do a "simple" bake, location only, world space only.
        locBake(s_frame, e_frame, simpleBake)


#Calls the baking function and decativates relevant constraints
def bakeConstraints(context):
    obj = context.active_object
    bones = obj.pose.bones
    s_frame, e_frame = context.scene.frame_start, context.scene.frame_end
    #Bake relevant bones
    bakeAllConstraints(obj, s_frame, e_frame, bones)
    for m_constraint in obj.data.mocap_constraints:
        end_bone = bones[m_constraint.real_constraint_bone]
        cons_obj = getConsObj(end_bone)
        # It's a control empty: turn the ik off
        if not isinstance(cons_obj, bpy.types.PoseBone):
            ik_con = retarget.hasIKConstraint(end_bone)
            if ik_con:
                ik_con.mute = True
        # Deactivate related Blender Constraint
        m_constraint.active = False


#Deletes the baked fcurves and reactivates relevant constraints
def unbakeConstraints(context):
    # to unbake constraints we delete the whole strip
    obj = context.active_object
    bones = obj.pose.bones
    scene = bpy.context.scene
    NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap]
    obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track]
    constraintTrack = obj.animation_data.nla_tracks[NLATracks.auto_fix_track]
    constraintStrip = constraintTrack.strips[0]
    action = constraintStrip.action
    # delete the fcurves on the strip
    for fcurve in action.fcurves:
        action.fcurves.remove(fcurve)
    # reactivate relevant constraints
    for m_constraint in obj.data.mocap_constraints:
        end_bone = bones[m_constraint.real_constraint_bone]
        cons_obj = getConsObj(end_bone)
        # It's a control empty: turn the ik back on
        if not isinstance(cons_obj, bpy.types.PoseBone):
            ik_con = retarget.hasIKConstraint(end_bone)
            if ik_con:
                ik_con.mute = False
        m_constraint.active = True


def updateConstraints(obj, context):
    fixes = obj.data.mocap_constraints
    for fix in fixes:
        fix.active = False
        fix.active = True
