using UnityEngine;
using System.Collections;
namespace RootMotion.FinalIK
{
    /// 
    /// Procedural recoil using FBBIK.
    /// 
    public class Recoil : OffsetModifier
    {
        [System.Serializable]
        public class RecoilOffset
        {
            [Tooltip("Offset vector for the associated effector when doing recoil.")]
            public Vector3 offset;
            [Tooltip("When firing before the last recoil has faded, how much of the current recoil offset will be maintained?")]
            [Range(0f, 1f)] public float additivity = 1f;
            [Tooltip("Max additive recoil for automatic fire.")]
            public float maxAdditiveOffsetMag = 0.2f;
            // Linking this to an effector
            [System.Serializable]
            public class EffectorLink
            {
                [Tooltip("Type of the FBBIK effector to use")]
                public FullBodyBipedEffector effector;
                [Tooltip("Weight of using this effector")]
                public float weight;
            }
            [Tooltip("Linking this recoil offset to FBBIK effectors.")]
            public EffectorLink[] effectorLinks;
            private Vector3 additiveOffset;
            private Vector3 lastOffset;
            // Start recoil
            public void Start()
            {
                if (additivity <= 0f) return;
                additiveOffset = Vector3.ClampMagnitude(lastOffset * additivity, maxAdditiveOffsetMag);
            }
            // Apply offset to FBBIK effectors
            public void Apply(IKSolverFullBodyBiped solver, Quaternion rotation, float masterWeight, float length, float timeLeft)
            {
                additiveOffset = Vector3.Lerp(Vector3.zero, additiveOffset, timeLeft / length);
                lastOffset = (rotation * (offset * masterWeight)) + (rotation * additiveOffset);
                foreach (EffectorLink e in effectorLinks)
                {
                    solver.GetEffector(e.effector).positionOffset += lastOffset * e.weight;
                }
            }
        }
        [System.Serializable]
        public enum Handedness
        {
            Right,
            Left
        }
        [Tooltip("Reference to the AimIK component. Optional, only used to getting the aiming direction.")]
        public AimIK aimIK;
        [Tooltip("Optional head AimIK solver. This solver should only use neck and head bones and have the head as Aim Transform")]
        public AimIK headIK;
        [Tooltip("Set this true if you are using IKExecutionOrder.cs or a custom script to force AimIK solve after FBBIK.")]
        public bool aimIKSolvedLast;
        [Tooltip("Which hand is holding the weapon?")]
        public Handedness handedness;
        [Tooltip("Check for 2-handed weapons.")]
        public bool twoHanded = true;
        [Tooltip("Weight curve for the recoil offsets. Recoil procedure is as long as this curve.")]
        public AnimationCurve recoilWeight;
        [Tooltip("How much is the magnitude randomized each time Recoil is called?")]
        public float magnitudeRandom = 0.1f;
        [Tooltip("How much is the rotation randomized each time Recoil is called?")]
        public Vector3 rotationRandom;
        [Tooltip("Rotating the primary hand bone for the recoil (in local space).")]
        public Vector3 handRotationOffset;
        [Tooltip("Time of blending in another recoil when doing automatic fire.")]
        public float blendTime;
        [Space(10)]
        [Tooltip("FBBIK effector position offsets for the recoil (in aiming direction space).")]
        public RecoilOffset[] offsets;
        [HideInInspector] public Quaternion rotationOffset = Quaternion.identity;
        private float magnitudeMlp = 1f;
        private float endTime = -1f;
        private Quaternion handRotation, secondaryHandRelativeRotation, randomRotation;
        private float length = 1f;
        private bool initiated;
        private float blendWeight;
        private float w;
        private Quaternion primaryHandRotation = Quaternion.identity;
        //private Quaternion secondaryHandRotation = Quaternion.identity;
        private bool handRotationsSet;
        private Vector3 aimIKAxis;
        /// 
        /// Returns true if recoil has finished or has not been called at all.
        /// 
        public bool isFinished
        {
            get
            {
                return Time.time > endTime;
            }
        }
        /// 
        /// Sets the starting rotations for the hands for 1 frame. Use this if the final rotation of the hands will not be the same as before FBBIK solves.
        /// 
        public void SetHandRotations(Quaternion leftHandRotation, Quaternion rightHandRotation)
        {
            if (handedness == Handedness.Left)
            {
                primaryHandRotation = leftHandRotation;
                //secondaryHandRotation = rightHandRotation;
            }
            else
            {
                primaryHandRotation = rightHandRotation;
                //secondaryHandRotation = leftHandRotation;
            }
            handRotationsSet = true;
        }
        /// 
        /// Starts the recoil procedure.
        /// 
        public void Fire(float magnitude)
        {
            float rnd = magnitude * UnityEngine.Random.value * magnitudeRandom;
            magnitudeMlp = magnitude + rnd;
            randomRotation = Quaternion.Euler(rotationRandom * UnityEngine.Random.value);
            foreach (RecoilOffset offset in offsets)
            {
                offset.Start();
            }
            if (Time.time < endTime) blendWeight = 0f;
            else blendWeight = 1f;
            Keyframe[] keys = recoilWeight.keys;
            length = keys[keys.Length - 1].time;
            endTime = Time.time + length;
        }
        protected override void OnModifyOffset()
        {
            if (aimIK != null) aimIKAxis = aimIK.solver.axis;
            if (!initiated && ik != null)
            {
                initiated = true;
                if (headIK != null) headIK.enabled = false;
                ik.solver.OnPostUpdate += AfterFBBIK;
                if (aimIK != null) aimIK.solver.OnPostUpdate += AfterAimIK;
            }
            if (Time.time >= endTime)
            {
                rotationOffset = Quaternion.identity;
                return;
            }
            blendTime = Mathf.Max(blendTime, 0f);
            if (blendTime > 0f) blendWeight = Mathf.Min(blendWeight + Time.deltaTime * (1f / blendTime), 1f);
            else blendWeight = 1f;
            // Current weight of offset
            float wTarget = recoilWeight.Evaluate(length - (endTime - Time.time)) * magnitudeMlp;
            w = Mathf.Lerp(w, wTarget, blendWeight);
            // Find the rotation space of the recoil
            Quaternion lookRotation = aimIK != null && aimIK.solver.transform != null && !aimIKSolvedLast ? Quaternion.LookRotation(aimIK.solver.IKPosition - aimIK.solver.transform.position, ik.references.root.up) : ik.references.root.rotation;
            lookRotation = randomRotation * lookRotation;
            // Apply FBBIK effector positionOffsets
            foreach (RecoilOffset offset in offsets)
            {
                offset.Apply(ik.solver, lookRotation, w, length, endTime - Time.time);
            }
            if (!handRotationsSet)
            {
                primaryHandRotation = primaryHand.rotation;
                //if (twoHanded) secondaryHandRotation = secondaryHand.rotation;
            }
            handRotationsSet = false;
            // Rotation offset of the primary hand
            rotationOffset = Quaternion.Lerp(Quaternion.identity, Quaternion.Euler(randomRotation * primaryHandRotation * handRotationOffset), w);
            handRotation = rotationOffset * primaryHandRotation;
            // Fix the secondary hand relative to the primary hand
            if (twoHanded)
            {
                Vector3 secondaryHandRelativePosition = Quaternion.Inverse(primaryHand.rotation) * (secondaryHand.position - primaryHand.position);
                secondaryHandRelativeRotation = Quaternion.Inverse(primaryHand.rotation) * secondaryHand.rotation;
                Vector3 primaryHandPosition = primaryHand.position + primaryHandEffector.positionOffset;
                Vector3 secondaryHandPosition = primaryHandPosition + handRotation * secondaryHandRelativePosition;
                secondaryHandEffector.positionOffset += secondaryHandPosition - (secondaryHand.position + secondaryHandEffector.positionOffset);
            }
            if (aimIK != null && aimIKSolvedLast) aimIK.solver.axis = Quaternion.Inverse(ik.references.root.rotation) * Quaternion.Inverse(rotationOffset) * aimIKAxis;
        }
        private void AfterFBBIK()
        {
            if (Time.time < endTime)
            {
                // Rotate the hand bones
                primaryHand.rotation = handRotation;
                if (twoHanded) secondaryHand.rotation = primaryHand.rotation * secondaryHandRelativeRotation;
            }
            if (!aimIKSolvedLast && headIK != null) headIK.solver.Update();
        }
        private void AfterAimIK()
        {
            if (aimIKSolvedLast) aimIK.solver.axis = aimIKAxis;
            if (aimIKSolvedLast && headIK != null) headIK.solver.Update();
        }
        // Shortcuts
        private IKEffector primaryHandEffector
        {
            get
            {
                if (handedness == Handedness.Right) return ik.solver.rightHandEffector;
                return ik.solver.leftHandEffector;
            }
        }
        private IKEffector secondaryHandEffector
        {
            get
            {
                if (handedness == Handedness.Right) return ik.solver.leftHandEffector;
                return ik.solver.rightHandEffector;
            }
        }
        private Transform primaryHand
        {
            get
            {
                return primaryHandEffector.bone;
            }
        }
        private Transform secondaryHand
        {
            get
            {
                return secondaryHandEffector.bone;
            }
        }
        protected override void OnDestroy()
        {
            base.OnDestroy();
            if (ik != null && initiated)
            {
                ik.solver.OnPostUpdate -= AfterFBBIK;
                if (aimIK != null) aimIK.solver.OnPostUpdate -= AfterAimIK;
            }
        }
    }
}