using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using RootMotion;

namespace RootMotion.FinalIK {

	/// <summary>
	/// Managing Interactions for a single FBBIK effector.
	/// </summary>
	[System.Serializable]
	public class InteractionEffector {

		// The type of the effector
		public FullBodyBipedEffector effectorType { get; private set; }

		// Has the interaction been paused?
		public bool isPaused { get; private set; }
		// The current InteractionObject (null if there is no interaction going on)
		public InteractionObject interactionObject { get; private set; }
		// Is this InteractionEffector currently in the middle of an interaction?
		public bool inInteraction { get { return interactionObject != null; }}

		// Internal values
		private Poser poser;
		private IKEffector effector;
		private float timer, length, weight, fadeInSpeed, defaultPositionWeight, defaultRotationWeight, defaultPull, defaultReach, defaultPush, defaultPushParent, defaultBendGoalWeight, defaultPoserWeight, resetTimer;
		private bool positionWeightUsed, rotationWeightUsed, pullUsed, reachUsed, pushUsed, pushParentUsed, bendGoalWeightUsed, poserUsed;
		private bool pickedUp, defaults, pickUpOnPostFBBIK;
		private Vector3 pickUpPosition, pausePositionRelative;
		private Quaternion pickUpRotation, pauseRotationRelative;
		private InteractionTarget interactionTarget;
		private Transform target;
		private List<bool> triggered = new List<bool>();
		private InteractionSystem interactionSystem;
		private bool started;

		// The custom constructor
		public InteractionEffector (FullBodyBipedEffector effectorType) {
			this.effectorType = effectorType;
		}

		// Initiate this, get the default values
		public void Initiate(InteractionSystem interactionSystem) {
			this.interactionSystem = interactionSystem;

			// Find the effector if we haven't already
			effector = interactionSystem.ik.solver.GetEffector(effectorType);
			poser = effector.bone.GetComponent<Poser>();

			StoreDefaults();
		}

        /// <summary>
        /// Store the default values to which the effector will be reset to after an interaction has ended.
        /// </summary>
		public void StoreDefaults() {
            if (interactionSystem == null) return;

			defaultPositionWeight = interactionSystem.ik.solver.GetEffector(effectorType).positionWeight;
			defaultRotationWeight = interactionSystem.ik.solver.GetEffector(effectorType).rotationWeight;
			defaultPoserWeight = poser != null? poser.weight: 0f;
			defaultPull = interactionSystem.ik.solver.GetChain(effectorType).pull;
			defaultReach = interactionSystem.ik.solver.GetChain(effectorType).reach;
			defaultPush = interactionSystem.ik.solver.GetChain(effectorType).push;
			defaultPushParent = interactionSystem.ik.solver.GetChain(effectorType).pushParent;
            defaultBendGoalWeight = interactionSystem.ik.solver.GetChain(effectorType).bendConstraint.weight;
		}

		// Interpolate to default values when currently not in interaction
		public bool ResetToDefaults(float speed, float deltaTime) {
			if (inInteraction) return false;
			if (isPaused) return false;
			if (defaults) return false; 

			resetTimer = Mathf.MoveTowards(resetTimer, 0f, deltaTime * speed);

			// Pull and Reach
			if (effector.isEndEffector) {
				if (pullUsed) interactionSystem.ik.solver.GetChain(effectorType).pull = Mathf.Lerp(defaultPull, interactionSystem.ik.solver.GetChain(effectorType).pull, resetTimer);
				if (reachUsed) interactionSystem.ik.solver.GetChain(effectorType).reach = Mathf.Lerp(defaultReach, interactionSystem.ik.solver.GetChain(effectorType).reach, resetTimer);
				if (pushUsed) interactionSystem.ik.solver.GetChain(effectorType).push = Mathf.Lerp(defaultPush, interactionSystem.ik.solver.GetChain(effectorType).push, resetTimer);
				if (pushParentUsed) interactionSystem.ik.solver.GetChain(effectorType).pushParent = Mathf.Lerp(defaultPushParent, interactionSystem.ik.solver.GetChain(effectorType).pushParent, resetTimer);
                if (bendGoalWeightUsed) interactionSystem.ik.solver.GetChain(effectorType).bendConstraint.weight = Mathf.Lerp(defaultBendGoalWeight, interactionSystem.ik.solver.GetChain(effectorType).bendConstraint.weight, resetTimer);
            }

			// Effector weights
			if (positionWeightUsed) effector.positionWeight = Mathf.Lerp(defaultPositionWeight, effector.positionWeight, resetTimer);
			if (rotationWeightUsed) effector.rotationWeight = Mathf.Lerp(defaultRotationWeight, effector.rotationWeight, resetTimer);

			if (resetTimer <= 0f) {
				pullUsed = false;
				reachUsed = false;
				pushUsed = false;
				pushParentUsed = false;
				positionWeightUsed = false;
				rotationWeightUsed = false;
                bendGoalWeightUsed = false;
				poserUsed = false;

				defaults = true;
			}

			return true;
		}

		// Pause this interaction
		public bool Pause() {
			if (!inInteraction) return false;
			isPaused = true;

			pausePositionRelative = target.InverseTransformPoint(effector.position);
			pauseRotationRelative = Quaternion.Inverse(target.rotation) * effector.rotation;

			if (interactionSystem.OnInteractionPause != null) {
				interactionSystem.OnInteractionPause(effectorType, interactionObject);
			}

			return true;
		}

		// Resume a paused interaction
		public bool Resume() {
			if (!inInteraction) return false;

			isPaused = false;
			if (interactionSystem.OnInteractionResume != null) interactionSystem.OnInteractionResume(effectorType, interactionObject);

			return true;
		}

        // Start interaction
        public bool Start(InteractionObject interactionObject, string tag, float fadeInTime, bool interrupt)
        {
            // Get the InteractionTarget
            InteractionTarget interactionTarget = null;
            target = interactionObject.GetTarget(effectorType, tag);
            if (target != null) interactionTarget = target.GetComponent<InteractionTarget>();
            
            return Start(interactionObject, interactionTarget, fadeInTime, interrupt);
        }

        public bool Start(InteractionObject interactionObject, InteractionTarget interactionTarget, float fadeInTime, bool interrupt)
        {
            this.interactionTarget = interactionTarget;
            
            // If not in interaction, set effector positions to their bones
            if (!inInteraction)
            {
                effector.position = effector.bone.position;
                effector.rotation = effector.bone.rotation;
            }
            else
            {
                if (!interrupt) return false;
                else defaults = false;
            }
            
            target = interactionTarget != null? interactionTarget.transform: interactionObject.transform;

            // Start the interaction
            this.interactionObject = interactionObject;
            if (interactionSystem.OnInteractionStart != null) interactionSystem.OnInteractionStart(effectorType, interactionObject);
            interactionObject.OnStartInteraction(interactionSystem);

            // Cleared triggered events
            triggered.Clear();

            for (int i = 0; i < interactionObject.events.Length; i++)
            {
                triggered.Add(false);
            }

            // See which InteractionObject.WeightCurve.Types are used
            positionWeightUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.PositionWeight);
            rotationWeightUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.RotationWeight);
            pullUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.Pull);
            reachUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.Reach);
            pushUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.Push);
            pushParentUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.PushParent);
            bendGoalWeightUsed = interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.BendGoalWeight);
            poserUsed = poser != null && interactionObject.CurveUsed(InteractionObject.WeightCurve.Type.PoserWeight);

            // Posing the hand/foot
            if (poser != null && poserUsed)
            {
                if (poser.poseRoot == null) poser.weight = 0f;

                if (interactionTarget != null)
                {
					if (interactionTarget.usePoser)
					{
						poser.poseRoot = target.transform;
						poser.AutoMapping(interactionTarget.bones);
					}
                } else
                {
                    poser.poseRoot = null;
                }

                //if (interactionTarget != null) poser.poseRoot = target.transform;
                //else poser.poseRoot = null;

                poser.AutoMapping();
            }

            if (defaults) StoreDefaults();

            // Reset internal values
            timer = 0f;
            weight = 0f;
            fadeInSpeed = fadeInTime > 0f ? 1f / fadeInTime : 1000f;
            length = interactionObject.length;

            isPaused = false;
            pickedUp = false;
            pickUpPosition = Vector3.zero;
            pickUpRotation = Quaternion.identity;

            if (interactionTarget != null) interactionTarget.RotateTo(effector.bone);

            started = true;

            return true;
        }

		// Update the (possibly) ongoing interaction
		public void Update(Transform root, float speed, float deltaTime) {
			if (!inInteraction) {
				// If the InteractionObject has been destroyed, reset to defaults
				if (started) {
					isPaused = false;
					pickedUp = false;
					defaults = false;
					resetTimer = 1f;
					started = false;
				}
				return;
			}

			// Rotate target
			if (interactionTarget != null && !interactionTarget.rotateOnce) interactionTarget.RotateTo(effector.bone);

			if (isPaused) {
                if (!pickedUp)
                {
                    effector.position = target.TransformPoint(pausePositionRelative);
                    effector.rotation = target.rotation * pauseRotationRelative;
                }
				// Apply the current interaction state to the solver
				interactionObject.Apply(interactionSystem.ik.solver, effectorType, interactionTarget, timer, weight, true);

				return;
			}

			// Advance the interaction timer and weight
			timer += deltaTime * speed * (interactionTarget != null? interactionTarget.interactionSpeedMlp: 1f);
			weight = Mathf.Clamp(weight + deltaTime * fadeInSpeed * speed, 0f, 1f);

			// Interaction events
			bool pickUp = false;
			bool pause = false;
			TriggerUntriggeredEvents(true, out pickUp, out pause);

			// Effector target positions and rotations
			Vector3 targetPosition = pickedUp? interactionSystem.transform.TransformPoint(pickUpPosition): target.position;
			Quaternion targetRotation = pickedUp? interactionSystem.transform.rotation * pickUpRotation: target.rotation;

			// Interpolate effector position and rotation
			effector.position = Vector3.Lerp(effector.bone.position, targetPosition, weight);
			effector.rotation = Quaternion.Lerp(effector.bone.rotation, targetRotation, weight);

			// Apply the current interaction state to the solver
			interactionObject.Apply(interactionSystem.ik.solver, effectorType, interactionTarget, timer, weight, false);

            if (pickUp) PickUp(root);
            if (pause) Pause();

            // Hand poser weight
            float poserWeight = interactionObject.GetValue (InteractionObject.WeightCurve.Type.PoserWeight, interactionTarget, timer);

			if (poser != null && poserUsed) {
				poser.weight = Mathf.Lerp (poser.weight, poserWeight, weight);
			} else {
				if (poserWeight > 0f) {
					Warning.Log("InteractionObject " + interactionObject.name + " has a curve/multipler for Poser Weight, but the bone of effector " + effectorType.ToString() + " has no HandPoser/GenericPoser attached.", effector.bone);
				}
			}

			if (timer >= length) Stop();
		}

		// Get the normalized progress of the interaction
		public float progress {
			get {
				if (!inInteraction) return 0f;
				if (length == 0f) return 0f;
				return timer / length;
			}
		}

		// Go through all the InteractionObject events to trigger the ones that have not yet triggered
		private void TriggerUntriggeredEvents(bool checkTime, out bool pickUp, out bool pause) {
			pickUp = false;
			pause = false;

			for (int i = 0; i < triggered.Count; i++) {
				// If this event has not been triggered by this effector
				if (!triggered[i]) {
					
					// If time has passed...
					if (!checkTime || interactionObject.events[i].time < timer) {
						
						// Activate the event
						interactionObject.events[i].Activate(effector.bone);
						
						// Picking up
						if (interactionObject.events[i].pickUp) {
							if (timer >= interactionObject.events[i].time) timer = interactionObject.events[i].time;

							pickUp = true;
						}
						
						// Pausing
						if (interactionObject.events[i].pause) {
							if (timer >= interactionObject.events[i].time) timer = interactionObject.events[i].time;

							pause = true;
						}
						
						if (interactionSystem.OnInteractionEvent != null) interactionSystem.OnInteractionEvent(effectorType, interactionObject, interactionObject.events[i]);
						
						triggered[i] = true;
					}
				}
			}
		}

		// Trigger the interaction object
		private void PickUp(Transform root) {
			// Picking up the object
			pickUpPosition = root.InverseTransformPoint(effector.position);
			pickUpRotation = Quaternion.Inverse(interactionSystem.transform.rotation) * effector.rotation;
				
			pickUpOnPostFBBIK = true;

			pickedUp = true;

			var rigidbody = interactionObject.targetsRoot.GetComponent<Rigidbody>();

			if (rigidbody != null) {
				if (!rigidbody.isKinematic) {
					rigidbody.isKinematic = true;
				}

                // Ignore collisions between the character and the colliders of the interaction object
                var rootCollider = root.GetComponent<Collider>();

				if (rootCollider != null) {
					var colliders = interactionObject.targetsRoot.GetComponentsInChildren<Collider>();

					foreach (Collider collider in colliders) {
						if (!collider.isTrigger && collider.enabled) Physics.IgnoreCollision(rootCollider, collider);
					}
				}
			}
				
			if (interactionSystem.OnInteractionPickUp != null) interactionSystem.OnInteractionPickUp(effectorType, interactionObject);
		}

		// Stop the interaction
		public bool Stop() {
			if (!inInteraction) return false;

			bool pickUp = false;
			bool pause = false;
			TriggerUntriggeredEvents(false, out pickUp, out pause);

			if (interactionSystem.OnInteractionStop != null) interactionSystem.OnInteractionStop(effectorType, interactionObject);

			// Reset the interaction target
			if (interactionTarget != null) interactionTarget.ResetRotation();

			// Reset the internal values
			interactionObject = null;
			weight = 0f;
			timer = 0f;

			isPaused = false;
			target = null;
			defaults = false;
			resetTimer = 1f;
			//if (poser != null && !pickedUp) poser.weight = 0f;
			pickedUp = false;
			started = false;

			return true;
		}

		// Called after FBBIK update
		public void OnPostFBBIK() {
			if (!inInteraction) return;

			// Rotate the hands/feet to the RotateBoneWeight curve
			float rotateBoneWeight = interactionObject.GetValue(InteractionObject.WeightCurve.Type.RotateBoneWeight, interactionTarget, timer) * weight;

			if (rotateBoneWeight > 0f) {
				Quaternion r = pickedUp? interactionSystem.transform.rotation * pickUpRotation: effector.rotation;

				Quaternion targetRotation = Quaternion.Slerp(effector.bone.rotation, r, rotateBoneWeight * rotateBoneWeight);
				effector.bone.localRotation = Quaternion.Inverse(effector.bone.parent.rotation) * targetRotation;
			}

			// Positioning the interaction object to the effector (not the bone, because it is still at its animated translation)
			if (pickUpOnPostFBBIK) {
				Vector3 bonePosition = effector.bone.position;
				effector.bone.position = interactionSystem.transform.TransformPoint(pickUpPosition);

				interactionObject.targetsRoot.parent = effector.bone;
				
				effector.bone.position = bonePosition;

				pickUpOnPostFBBIK = false;
			}
		}
	}
}
