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

/// <summary>
/// Collection of methods for working with splines.
/// This is based on the great learning tutorial Curves and Splines by Jasper Flick.
/// 
/// References:
/// http://catlikecoding.com/unity/tutorials/curves-and-splines/
/// http://answers.unity3d.com/questions/374333/positioning-an-object-on-a-spline-relative-to-play.html
/// </summary>
namespace PlaygroundSplines {

	/// <summary>
	/// Holds information about a spline and contains functions for working with the nodes and bezier handles.
	/// </summary>
	[ExecuteInEditMode()]
	public class PlaygroundSpline : MonoBehaviour {
		/// <summary>
		/// The list of nodes and bezier handles making the spline.
		/// </summary>
		[SerializeField]
		private List<Vector3> points = new List<Vector3>();
		/// <summary>
		/// The modes of the bezier handles.
		/// </summary>
		[SerializeField]
		private List<BezierControlPointMode> modes = new List<BezierControlPointMode>();
		/// <summary>
		/// Determines if the spline is looping.
		/// </summary>
		[SerializeField]
		private bool loop;

		/// <summary>
		/// The list of transform nodes to set positions live of an existing node.
		/// </summary>
		[HideInInspector] public List<TransformNode> transformNodes = new List<TransformNode>();
		/// <summary>
		/// Determines if the spline time should be reversed. If you'd like to physically reverse the arrays making the spline then call ReverseAllNodes().
		/// </summary>
		[HideInInspector] public bool reverse;
		/// <summary>
		/// The time offset of the spline.
		/// </summary>
		[HideInInspector] public float timeOffset;
		/// <summary>
		/// The position offset of the spline in relation to its transform.
		/// </summary>
		[HideInInspector] public Vector3 positionOffset;

		[HideInInspector] public Transform splineTransform;
		[HideInInspector] public Matrix4x4 splineTransformMx;
		[HideInInspector] public List<Transform> usedBy = new List<Transform>();
		[HideInInspector] public float fixedVelocityOnNewNode = .5f;
		[HideInInspector] public bool moveTransformsAsBeziers = false;
		[HideInInspector] public bool exportWithNodeStructure = false;

		// Gizmos
		public static bool drawSplinePreviews = true;
		[HideInInspector] public bool drawGizmo = true;
		[HideInInspector] public float bezierWidth = 2f;

		#if UNITY_EDITOR
		void OnDrawGizmos () {
			if (drawSplinePreviews && drawGizmo) {
				Color innerBezier = new Color(1f,1f,1f,1f);
				Color outerBezier = new Color(.5f,.5f,0,.2f);
				Vector3 p0 = ShowPoint(0);
				for (int i = 1; i < ControlPointCount; i += 3) {
					Vector3 p1 = ShowPoint(i);
					Vector3 p2 = ShowPoint(i + 1);
					Vector3 p3 = ShowPoint(i + 2);
					UnityEditor.Handles.DrawBezier(p0, p3, p1, p2, innerBezier, null, bezierWidth);
					UnityEditor.Handles.DrawBezier(p0, p3, p1, p2, outerBezier, null, bezierWidth*10f);
					p0 = p3;
				}
			}
		}
		Vector3 ShowPoint (int index) {
			return transformNodes[index].IsAvailable()? GetPoint(index)+positionOffset : splineTransform.TransformPoint(GetInversePoint(index)+positionOffset);
		}
		#endif

		Vector3 previousPosition;
		Quaternion previousRotation;
		Vector3 previousScale;
		bool isReady;

		public bool IsReady () {
			return isReady;
		}

		/// <summary>
		/// Adds a user to the spline. This helps keeping track of which objects are using the spline.
		/// </summary>
		/// <returns><c>true</c>, if user was added, <c>false</c> otherwise.</returns>
		/// <param name="">.</param>
		public bool AddUser (Transform thisTransform) {
			if (!usedBy.Contains(thisTransform)) {
				usedBy.Add (thisTransform);
				return true;
			}
			return false;
		}

		/// <summary>
		/// Removes a user from the spline. This helps keeping track of which objects are using the spline.
		/// </summary>
		/// <returns><c>true</c>, if user was removed, <c>false</c> otherwise.</returns>
		/// <param name="">.</param>
		public bool RemoveUser (Transform thisTransform) {
			if (usedBy.Contains(thisTransform)) {
				usedBy.Remove (thisTransform);
				return true;
			}
			return false;
		}

		/// <summary>
		/// Determines whether this spline has the user of passed in transform.
		/// </summary>
		/// <returns><c>true</c> if this spline has the user of the passed in transform; otherwise, <c>false</c>.</returns>
		/// <param name="thisTransform">This transform.</param>
		public bool HasUser (Transform thisTransform) {
			return usedBy.Contains (thisTransform);
		}

		/// <summary>
		/// Gets or sets a value indicating whether this <see cref="PlaygroundSplines.PlaygroundSpline"/> is set to loop.
		/// </summary>
		/// <value><c>true</c> if set to loop; otherwise, <c>false</c>.</value>
		public bool Loop {
			get {
				return loop;
			}
			set {
				loop = value;
				if (value == true && NodeCount>1) {
					modes[modes.Count - 1] = modes[0];
					SetControlPoint(0, points[0]);
				}
			}
		}


		/// <summary>
		/// Gets the control point count.
		/// </summary>
		/// <value>The control point count.</value>
		public int ControlPointCount {
			get {
				return points.Count;
			}
		}

		/// <summary>
		/// Gets the control point.
		/// </summary>
		/// <returns>The control point.</returns>
		/// <param name="index">Index.</param>
		public Vector3 GetControlPoint (int index) {
			return GetPoint(index);
		}

		/// <summary>
		/// Sets the control point and withdraws the offset.
		/// </summary>
		/// <param name="index">Index.</param>
		/// <param name="point">Point.</param>
		/// <param name="offset">Offset.</param>
		public void SetControlPoint (int index, Vector3 point, Vector3 offset) {
			SetControlPoint(index, point-offset);
		}

		/// <summary>
		/// Sets the control point.
		/// </summary>
		/// <param name="index">Index.</param>
		/// <param name="point">Position.</param>
		public void SetControlPoint (int index, Vector3 point) {
			if (index<0) index = 0;
			if (index % 3 == 0) {
				Vector3 delta = (point - GetPoint(index));
				Vector3 v;
				if (loop) {
					if (index == 0) {
						//if (!PointHasTransform(1))
						{v = GetPoint(1); SetPoint(1, v+delta);}
						//if (!PointHasTransform(points.Count-2))
						{v = GetPoint(points.Count-2); SetPoint(points.Count-2, v+delta);}
						if (moveTransformsAsBeziers || !PointHasTransform(points.Count-1))
						{SetPoint(points.Count-1, point);}
					} else 
					if (index == points.Count - 1) {
						//if (!PointHasTransform(0))
						{SetPoint(0, point);}
						//if (!PointHasTransform(1))
						{v = GetPoint(1); SetPoint(1, v+delta);}
						//if (!PointHasTransform(index-1))
						{v = GetPoint(index-1); SetPoint(index-1, v+delta);}
					} else {
						//if (!PointHasTransform(index-1))
						{v = GetPoint(index-1); SetPoint(index-1, v+delta);}
						//if (!PointHasTransform(index+1))
						{v = GetPoint(index+1); SetPoint(index+1, v+delta);}
					}
				} else {
					if (index > 0) {
						if (moveTransformsAsBeziers || !PointHasTransform(index-1))
						{v = GetPoint(index-1); SetPoint(index-1, v+delta);}
					}
					if (index + 1 < points.Count) {
						if (moveTransformsAsBeziers || !PointHasTransform(index+1))
						{v = GetPoint(index+1); SetPoint(index+1, v+delta);}
					}
				}
			}
			SetPoint(index, point);
			EnforceMode(index);
		}

		/// <summary>
		/// Sets all points from an array. Please ensure the same length of your passed in vectors as PlaygroundSpline.ControlPointCount.
		/// </summary>
		/// <param name="vectors">Vectors.</param>
		public void SetPoints (Vector3[] vectors) {
			if (vectors.Length!=points.Count) {
				Debug.Log ("Please ensure the same length of your passed in vectors ("+vectors.Length+") as the current points ("+points.Count+"). Use PlaygroundSpline.ControlPointCount to get the current count.");
				return;
			}
			for (int i = 0; i<points.Count; i++) {
				points[i] = vectors[i];
			}
		}

		public bool PointHasTransform (int index) {
			return transformNodes[index].IsAvailable();
		}

		/// <summary>
		/// Moves the entire spline separate from its transform component. Use this if you'd like to offset the spline from its transform separately from the positionOffset.
		/// </summary>
		/// <param name="translation">The amount to move the spline in Units.</param>
		public void TranslateSpline (Vector3 translation) {
			for (int i = 0; i<points.Count; i++) {
				points[i] += translation;
			}
		}

		public Vector3 GetTransformPosition () {
			return previousPosition;
		}
		public Quaternion GetTransformRotation () {
			return previousRotation;
		}
		public Vector3 GetTransformScale () {
			return previousScale;
		}
		
		public BezierControlPointMode GetControlPointMode (int index) {
			return modes[(index + 1) / 3];
		}
		
		public void SetControlPointMode (int index, BezierControlPointMode mode) {
			int modeIndex = (index + 1) / 3;
			modes[modeIndex] = mode;
			if (loop) {
				if (modeIndex == 0) {
					modes[modes.Count - 1] = mode;
				}
				else if (modeIndex == modes.Count - 1) {
					modes[0] = mode;
				}
			}
			EnforceMode(index);
		}
		
		private void EnforceMode (int index) {
			int modeIndex = (index+1) / 3;
			BezierControlPointMode mode = modes[modeIndex];
			if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Count - 1)) {
				return;
			}
			
			int middleIndex = modeIndex * 3;
			int fixedIndex, enforcedIndex;
			if (index <= middleIndex) {
				fixedIndex = middleIndex - 1;
				if (fixedIndex < 0) {
					fixedIndex = points.Count - 2;
				}
				enforcedIndex = middleIndex + 1;
				if (enforcedIndex >= points.Count) {
					enforcedIndex = 1;
				}
			}
			else {
				fixedIndex = middleIndex + 1;
				if (fixedIndex >= points.Count) {
					fixedIndex = 1;
				}
				enforcedIndex = middleIndex - 1;
				if (enforcedIndex < 0) {
					enforcedIndex = points.Count - 2;
				}
			}
			
			Vector3 middle = GetPoint(middleIndex);
			Vector3 enforcedTangent = middle - GetPoint(fixedIndex);
			if (mode == BezierControlPointMode.Aligned) {
				enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, GetPoint(enforcedIndex));
			}
			if (moveTransformsAsBeziers || !PointHasTransform(enforcedIndex))
				SetPoint(enforcedIndex, middle + enforcedTangent);
		}
		
		public int NodeCount {
			get {
				return (points.Count - 1) / 3;
			}
		}

		/// <summary>
		/// Get position from time.
		/// </summary>
		/// <returns>The point in world space.</returns>
		/// <param name="t">Time.</param>
		public Vector3 GetPoint (float t) {
			int i;
			if (reverse) {
				t = 1f-t;
				t = (t-timeOffset)%1f;
				if (t<0)
					t = 1f+t;
			} else t = (t+timeOffset)%1f;

			if (t >= 1f) {
				//t = 1f;
				i = points.Count - 4;
			}
			else {
				t = Mathf.Clamp01(t) * NodeCount;
				i = (int)t;
				t -= i;
				i *= 3;
			}
			return splineTransformMx.MultiplyPoint3x4(Bezier.GetPoint(GetInversePoint(i), GetInversePoint(i + 1), GetInversePoint(i + 2), GetInversePoint(i + 3), t)+positionOffset);
		}

		public Vector3 GetVelocity (float t) {
			int i;
			if (reverse)
				t = 1f-t;
			t = (t+timeOffset)%1f;
			if (t >= 1f) {
				t = 1f;
				i = points.Count - 4;
			}
			else {
				t = Mathf.Clamp01(t) * NodeCount;
				i = (int)t;
				t -= i;
				i *= 3;
			}
			return splineTransformMx.MultiplyPoint3x4(Bezier.GetFirstDerivative(GetInversePoint(i), GetInversePoint(i + 1), GetInversePoint(i + 2), GetInversePoint(i + 3), t)+positionOffset) - previousPosition;
		}

		/// <summary>
		/// Get position from node index in the spline. If the node consists of an available transform its position will be returned, otherwise the user-specified Vector3 position.
		/// </summary>
		/// <returns>The point in world space.</returns>
		/// <param name="index">Index.</param>
		public Vector3 GetPoint (int index) {
			if (transformNodes[index].IsAvailable())
				return transformNodes[index].GetPosition();
			else return points[index];
		}

		public Vector3 GetInversePoint (int index) {
			if (transformNodes[index].IsAvailable())
				return transformNodes[index].GetInvsersePosition();
			else return points[index];
		}

		public Vector3 GetPointWorldSpace (int index) {
			if (transformNodes[index].IsAvailable())
				return transformNodes[index].GetPosition();
			else return splineTransformMx.MultiplyPoint3x4(points[index]+positionOffset);
		}

		/// <summary>
		/// Sets a point to specified position.
		/// </summary>
		/// <param name="index">Index.</param>
		/// <param name="position">Position.</param>
		void SetPoint (int index, Vector3 position) {
			if (transformNodes[index].IsAvailable())
				transformNodes[index].SetPosition(position);
			else points[index] = position;
		}

		/// <summary>
		/// Translates a point.
		/// </summary>
		/// <param name="index">Index.</param>
		/// <param name="translation">Translation.</param>
		void TranslatePoint (int index, Vector3 translation) {
			if (transformNodes[index].IsAvailable())
				transformNodes[index].Translate(translation);
			else points[index] += translation;
		}

		// Calculates the best fitting time in the given interval
		private float CPOB(Vector3 aP, float aStart, float aEnd, int aSteps)
		{
			aStart = Mathf.Clamp01(aStart);
			aEnd = Mathf.Clamp01(aEnd);
			float step = (aEnd-aStart) / (float)aSteps;
			float Res = 0;
			float Ref = float.MaxValue;
			for (int i = 0; i < aSteps; i++)
			{
				float t = aStart + step*i;
				float L = (GetPoint(t)-aP).sqrMagnitude;
				if (L < Ref)
				{
					Ref = L;
					Res = t;
				}
			}
			return Res;
		}
		
		public float ClosestTimeFromPoint (Vector3 aP) {
			float t = CPOB(aP, 0, 1, 10);
			float delta = 1.0f / 10.0f;
			for (int i = 0; i < 4; i++)
			{
				t = CPOB(aP, t - delta, t + delta, 10);
				delta /= 9;
			}
			return t;
		}
		
		public Vector3 ClosestPointFromPosition (Vector3 aP) {
			return GetPoint(ClosestTimeFromPoint(aP));
		}

		public Vector3 GetDirection (float t) {
			return (GetPoint(t+.001f)-GetPoint(t)).normalized;
		}

		/// <summary>
		/// Adds a node at the last position of the node index.
		/// </summary>
		public void AddNode () {
			AddNode ((points.Count-1)/3);
		}

		/// <summary>
		/// Adds a node at specified node index.
		/// </summary>
		/// <param name="index">Index.</param>
		public void AddNode (int index) {
			int nodeIndex = index*3;
			Vector3 point = GetPoint(nodeIndex);
			Vector3 direction;
			if (index>0) {
				direction = GetPoint(nodeIndex)-GetPoint(nodeIndex-1);
			} else direction = GetPoint(nodeIndex+1)-GetPoint(nodeIndex);

			direction*=fixedVelocityOnNewNode;

			points.InsertRange(nodeIndex+1, new Vector3[3]);
			point += direction;
			points[nodeIndex+2] = point;
			point += direction;
			points[nodeIndex+1] = point;
			point += direction;
			points[nodeIndex+3] = point;

			transformNodes.InsertRange(nodeIndex+1, new TransformNode[]{new TransformNode(), new TransformNode(), new TransformNode()});

			BezierControlPointMode currentIndexMode = modes[index];
			modes.Insert (index, new BezierControlPointMode());
			modes[index] = currentIndexMode;
			EnforceMode(index);

			SetControlPoint((index+1)*3, GetPoint((index+1)*3));
			
			if (loop) {
				points[points.Count - 1] = points[0];
				modes[modes.Count - 1] = modes[0];
				EnforceMode(0);
			}
		}

		/// <summary>
		/// Removes the first node in the node index.
		/// </summary>
		public void RemoveFirst () {
			RemoveNode(0);
		}

		/// <summary>
		/// Removes the last node in the node index.
		/// </summary>
		public void RemoveLast () {
			RemoveNode((points.Count-1)/3);
		}

		/// <summary>
		/// Removes a node at specified node index.
		/// </summary>
		/// <param name="index">Index.</param>
		public void RemoveNode (int index) {
			index = Mathf.Clamp (index, 0, points.Count-1);
			int pointIndex = index*3;
			if (points.Count<=4) return;
			if (pointIndex<points.Count-1) {
				points.RemoveRange(pointIndex, 3);
				transformNodes.RemoveRange(pointIndex, 3);
			} else {
				points.RemoveRange(pointIndex-2, 3);
				transformNodes.RemoveRange(pointIndex-2, 3);
			}
			modes.RemoveAt (index);
			EnforceMode (index-1);
			if (index>0)
				SetControlPoint((index-1)*3, GetPoint((index-1)*3));
			else
				SetControlPoint(0, GetPoint(0));
		}

		/// <summary>
		/// Reverses all nodes in the node index.
		/// </summary>
		public void ReverseAllNodes () {
			points.Reverse();
			transformNodes.Reverse();
			modes.Reverse();
		}

		public void SwapNodes (int from, int to) {
			Vector3[] fromPoints = points.GetRange (from, 3).ToArray();
			Vector3[] toPoints = points.GetRange (to, 3).ToArray();
			TransformNode[] fromTnode = transformNodes.GetRange (from, 3).ToArray();
			TransformNode[] toTnode = transformNodes.GetRange (to, 3).ToArray();
			BezierControlPointMode fromMode = modes[from];
			BezierControlPointMode toMode = modes[to];

			for (int i = from; i<3; i++) {
				points[i] = toPoints[i];
				transformNodes[i] = toTnode[i];
			}
			for (int i = to; i<3; i++) {
				points[i] = fromPoints[i];
				transformNodes[i] = fromTnode[i];
			}
			modes[from] = toMode;
			modes[to] = fromMode;
		}

		/// <summary>
		/// Exports all nodes to Transform[]. Enable exportWithNodeStructure to parent each bezier handle to their node.
		/// </summary>
		/// <returns>A built-in array of Transforms.</returns>
		public Transform[] ExportToTransforms () {
			Transform[] transforms = new Transform[points.Count];
			for (int i = 0; i<points.Count; i++) {
				int iNode = (i+1)/3;
				int iBezier = i<3?0:(((i)%3))%2;
				bool iIsNode = i==0||i%3==0;
				transforms[i] = new GameObject(iIsNode?"Node "+iNode:"Node "+iNode+" - Bezier "+iBezier).transform;
				transforms[i].parent = splineTransform;
				transforms[i].position = splineTransform.TransformPoint(GetInversePoint(i)+positionOffset);
			}
			if (exportWithNodeStructure) {
				for (int i = 2; i<transforms.Length; i++) {
					int iBezier = i<3?0:(((i)%3))%2;
					bool iIsNode = i==0||i%3==0;
					if (!iIsNode)
						transforms[i].parent = transforms[iBezier==0?i+1:i-1];
				}
				transforms[1].parent = transforms[0];
			}
			return transforms;
		}

		/// <summary>
		/// Exports all nodes to Vector3[].
		/// </summary>
		/// <returns>A built-in array of Vector3</returns>
		public Vector3[] ExportToVector3 () {
			Vector3[] vectors = new Vector3[points.Count];
			for (int i = 0; i<points.Count; i++) {
				vectors[i] = GetPoint(i)+positionOffset;
			}
			return vectors;
		}

		/// <summary>
		/// Reset this Playground Spline. Two nodes and two bezier handles will be created. 
		/// </summary>
		public void Reset () {
			points = new List<Vector3> {
				new Vector3(1f, 0f, 0f),
				new Vector3(2f, 0f, 0f),
				new Vector3(3f, 0f, 0f),
				new Vector3(4f, 0f, 0f)
			};
			modes = new List<BezierControlPointMode> {
				BezierControlPointMode.Aligned,
				BezierControlPointMode.Aligned
			};
			transformNodes = new List<TransformNode> {
				new TransformNode(),
				new TransformNode(),
				new TransformNode(),
				new TransformNode()
			};
		}

		/*************************************************************************************************************************************************
			MonoBehaviours
		*************************************************************************************************************************************************/

		void OnEnable () {
			isReady = false;
			splineTransform = transform;
			SetMatrix();
		}

		void Update () {
			SetMatrix();
			for (int i = 0; i<transformNodes.Count; i++)
				transformNodes[i].Update(splineTransform);
		}

		void SetMatrix () {
			if (previousPosition!=splineTransform.position || previousRotation!=splineTransform.rotation || previousScale!=splineTransform.localScale)
				splineTransformMx.SetTRS(splineTransform.position, splineTransform.rotation, splineTransform.localScale);
			previousPosition = splineTransform.position;
			previousRotation = splineTransform.rotation;
			previousScale = splineTransform.localScale;
			isReady = true;
		}
	}

	/// <summary>
	/// Class for common bezier operations on a spline.
	/// </summary>
	public static class Bezier {
		
		public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
			t = Mathf.Clamp01(t);
			float oneMinusT = 1f - t;
			return
				oneMinusT * oneMinusT * p0 +
					2f * oneMinusT * t * p1 +
					t * t * p2;
		}
		
		public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
			return
				2f * (1f - t) * (p1 - p0) +
					2f * t * (p2 - p1);
		}
		
		public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
			t = Mathf.Clamp01(t);
			float OneMinusT = 1f - t;
			return
				OneMinusT * OneMinusT * OneMinusT * p0 +
					3f * OneMinusT * OneMinusT * t * p1 +
					3f * OneMinusT * t * t * p2 +
					t * t * t * p3;
		}
		
		public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
			t = Mathf.Clamp01(t);
			float oneMinusT = 1f - t;
			return
				3f * oneMinusT * oneMinusT * (p1 - p0) +
					6f * oneMinusT * t * (p2 - p1) +
					3f * t * t * (p3 - p2);
		}
	}

	[Serializable]
	public class TransformNode {
		public bool enabled;
		public Transform transform;
		bool isAvailable;
		Vector3 position;
		Vector3 inversePosition;
		Vector3 previousPosition;

		public bool Update (Transform splineTransform) {
			if (enabled && transform!=null) {
				previousPosition = position;
				position = transform.position;
				inversePosition = splineTransform.InverseTransformPoint(transform.position);
				isAvailable = true;
				return true;
			}
			isAvailable = false;
			return false;
		}

		public bool IsAvailable () {
			return enabled&&isAvailable;
		}

		public Vector3 GetPosition () {
			return position;
		}

		public Vector3 GetInvsersePosition () {
			return inversePosition;
		}

		public void SetPosition (Vector3 newPosition) {
			if (transform==null) return;
			transform.position = newPosition;
		}

		public void Translate (Vector3 translation) {
			if (transform==null) return;
			transform.position += translation;
		}

		public Vector3 GetPositionDelta () {
			return previousPosition-position;
		}
	}

	public enum SplineMode {
		Vector3,
		Transform
	}

	/// <summary>
	/// The bezier mode for a spline node. This controls how one bezier handle acts in relation to the other.
	/// </summary>
	public enum BezierControlPointMode {
		/// <summary>
		/// Align the angle between the two bezier handles but keep individual lengths. Has a differential smooth in and out angle.
		/// </summary>
		Aligned,
		/// <summary>
		/// Align the angle and length between the two bezier handles. Has an equally smooth in and out angle.
		/// </summary>
		Mirrored,
		/// <summary>
		/// Bezier handles are freely aligned without consideration to the other. Ables you to have sharp angles.
		/// </summary>
		Free
	}
}