using UnityEditor;
using UnityEngine;
using System.Collections;
using System;
using System.Reflection;

namespace RootMotion.FinalIK {

	/*
	 * Custom inspector for RotationLimitPolygonal
	 * */
	[CustomEditor(typeof(RotationLimitPolygonal))]
	[CanEditMultipleObjects]
	public class RotationLimitPolygonalInspector : RotationLimitInspector {
		
		/*
		 * Used for quick symmetric editing in the scene
		 * */
		public enum Symmetry {
			Off,
			X,
			Y,
			Z
		}
		
		private RotationLimitPolygonal script { get { return target as RotationLimitPolygonal; }}
		private RotationLimitPolygonal clone;
		private int selectedPoint = -1, deletePoint = -1, addPoint = -1;
		private float degrees = 90;
		private Symmetry symmetry;
		
		#region Inspector
		
		public void OnEnable() {
			// If initialized, set up the default polygon
			if (script.points == null || (script.points != null && script.points.Length < 3)) {
				script.ResetToDefault();
				EditorUtility.SetDirty(script);
			}
		}
		
		public override void OnInspectorGUI() {
			GUI.changed = false;
			
			// Clamping values
			script.twistLimit = Mathf.Clamp(script.twistLimit, 0, 180);
			script.smoothIterations = Mathf.Clamp(script.smoothIterations, 0, 3);
			
			DrawDefaultInspector();
			
			if (GUI.changed) EditorUtility.SetDirty(script);
		}
		
		#endregion Inspector
		
		#region Scene
		
		public void OnSceneGUI() {
			GUI.changed = false;
			
			// Set defaultLocalRotation so that the initial local rotation will be the zero point for the rotation limit
			if (!Application.isPlaying) script.defaultLocalRotation = script.transform.localRotation;
			if (script.axis == Vector3.zero) return;
			
			// Quick Editing Tools
			Handles.BeginGUI();		
			GUILayout.BeginArea(new Rect(10, Screen.height - 180, 550, 130), "Rotation Limit Polygonal", "Window");
			
			// Cloning values from another RotationLimitPolygonal
			EditorGUILayout.BeginHorizontal();
			if (Inspector.Button("Clone From", "Make this rotation limit identical to another", script, GUILayout.Width(220))) CloneLimit();
			clone = (RotationLimitPolygonal)EditorGUILayout.ObjectField("", clone, typeof(RotationLimitPolygonal), true);
			EditorGUILayout.EndHorizontal();
			
			// Symmetry
			symmetry = (Symmetry)EditorGUILayout.EnumPopup("Symmetry", symmetry, GUILayout.Width(220));
			
			// Flipping
			EditorGUILayout.BeginHorizontal();
			if (Inspector.Button("Flip X", "Flip points along local X axis", script, GUILayout.Width(100))) FlipLimit(0);
			if (Inspector.Button("Flip Y", "Flip points along local Y axis", script, GUILayout.Width(100))) FlipLimit(1);		
			if (Inspector.Button("Flip Z", "Flip points along local Z axis", script, GUILayout.Width(100))) FlipLimit(2);
			GUILayout.Label("Flip everything along axis");
			EditorGUILayout.EndHorizontal();
			
			// Rotating
			EditorGUILayout.BeginHorizontal();
			if (Inspector.Button("Rotate X", "Rotate points along X axis by Degrees", script, GUILayout.Width(100))) RotatePoints(degrees, Vector3.right);
			if (Inspector.Button("Rotate Y", "Rotate points along Y axis by Degrees", script, GUILayout.Width(100))) RotatePoints(degrees, Vector3.up);
			if (Inspector.Button("Rotate Z", "Rotate points along Z axis by Degrees", script, GUILayout.Width(100))) RotatePoints(degrees, Vector3.forward);
			
			degrees = EditorGUILayout.FloatField("Degrees", degrees, GUILayout.Width(200));
			EditorGUILayout.EndHorizontal();
			
			// Smooth/Optimize
			EditorGUILayout.BeginHorizontal();
			if (Inspector.Button("Smooth", "Double the points", script)) Smooth();
			if (Inspector.Button("Optimize", "Delete every second point", script)) Optimize();
			EditorGUILayout.EndHorizontal();

			GUILayout.EndArea();
			Handles.EndGUI();
			
			// Rebuild reach cones
			script.BuildReachCones();
			
			// Draw a white transparent sphere
			DrawRotationSphere(script.transform.position);
			
			// Draw Axis
			DrawArrow(script.transform.position, Direction(script.axis), colorDefault, "Axis", 0.02f);
		
			// Display limit points
			for (int i = 0; i < script.points.Length; i++) {
				Color color = GetColor(i); // Paint the point in green or red if it belongs to an invalid reach cone
				
				Handles.color = color;
				GUI.color = color;
				
				// Line from the center to the point and the label
				Handles.DrawLine(script.transform.position, script.transform.position + Direction(script.points[i].point));
				Handles.Label(script.transform.position + Direction(script.points[i].point + new Vector3(-0.02f, 0, 0)), " " + i.ToString());
				
				// Selecting points
				Handles.color = colorHandles;
				if (Handles.Button(script.transform.position + Direction(script.points[i].point), script.transform.rotation, 0.02f, 0.02f, Handles.DotCap)) {
					selectedPoint = i;
				}
				
				Handles.color = Color.white;
				GUI.color = Color.white;
				
				// Limit point GUI
				if (i == selectedPoint) {
					Handles.BeginGUI();
					
					GUILayout.BeginArea(new Rect(Screen.width - 240, Screen.height - 180, 230, 130), "Limit Point " + i.ToString(), "Window");
					
					if (Inspector.Button("Delete", "Delete this point", script)) {
						if (script.points.Length > 3) {
							// Using the deletePoint index here because we dont want to delete points from the array that we are iterating
							deletePoint = i;
						} else if (!Warning.logged) script.LogWarning("Polygonal Rotation Limit should have at least 3 limit points");
					}
					if (Inspector.Button("Add Point", "Add a new point next to this one", script)) {
						addPoint = i;
					}
					
					// Store point for undo
					Vector3 oldPoint = script.points[i].point;
					
					// Manual input for the point position
					Inspector.AddVector3(ref script.points[i].point, "Point", script, GUILayout.Width(210));
					
					EditorGUILayout.Space();
					
					// Tangent weight
					Inspector.AddFloat(ref script.points[i].tangentWeight, "Tangent Weight", "Weight of this point's tangent. Used in smoothing.", script, -Mathf.Infinity, Mathf.Infinity, GUILayout.Width(150));
					
					GUILayout.EndArea();
					
					Handles.EndGUI();
					
					// Moving Points
					Vector3 pointWorld = Handles.PositionHandle(script.transform.position + Direction(script.points[i].point), Quaternion.identity);
					Vector3 newPoint = InverseDirection(pointWorld - script.transform.position);
					if (newPoint != script.points[i].point) {
						if (!Application.isPlaying) Undo.RecordObject(script, "Move Limit Point");
						script.points[i].point = newPoint;
					}
					
					// Symmetry
					if (symmetry != Symmetry.Off && script.points.Length > 3 && oldPoint != script.points[i].point) {					
						RotationLimitPolygonal.LimitPoint symmetryPoint = GetClosestPoint(Symmetrize(oldPoint, symmetry));
						if (symmetryPoint != script.points[i]) {
							symmetryPoint.point = Symmetrize(script.points[i].point, symmetry);
						}
					}
				}
				
				// Normalize the point
				script.points[i].point = script.points[i].point.normalized;
			}
			
			// Display smoothed polygon
			for (int i = 0; i < script.P.Length; i++) {
				Color color = GetColor(i);
				
				// Smoothed triangles are transparent
				Handles.color = new Color(color.r, color.g, color.b, 0.25f);
				Handles.DrawLine(script.transform.position, script.transform.position + Direction(script.P[i]));
				
				Handles.color = color;
				
				if (i < script.P.Length - 1) Handles.DrawLine(script.transform.position + Direction(script.P[i]), script.transform.position + Direction(script.P[i + 1]));
				else Handles.DrawLine(script.transform.position + Direction(script.P[i]), script.transform.position + Direction(script.P[0]));
				
				Handles.color = Color.white;
			}

			// Deleting points
			if (deletePoint != -1) {
				DeletePoint(deletePoint);
				selectedPoint = -1;
				deletePoint = -1;
			}
			
			// Adding points
			if (addPoint != -1) {
				AddPoint(addPoint);
				addPoint = -1;
			}
			
			if (GUI.changed) EditorUtility.SetDirty(script);
		}
		
		private Color GetColor(int i) {
			// Paint the polygon in red if the reach cone is invalid
			return script.reachCones[i].isValid? colorDefault: colorInvalid;
		}
		
		/*
		 * Doubles the number of Limit Points
		 * */
		private void Smooth() {
			int length = script.points.Length;
			for (int i = 0; i < length; i++) {
				AddPoint(i + i);
			}
		}
		
		/*
		 * Reduces the number of Limit Points
		 * */
		private void Optimize() {
			for (int i = 1; i < script.points.Length; i++) {
				if (script.points.Length > 3) DeletePoint(i);
			}
		}
		
		/*
		 * Flips the rotation limit along the axis
		 * */
		private void FlipLimit(int axis) {
			script.axis[axis] = -script.axis[axis];
			
			foreach (RotationLimitPolygonal.LimitPoint limitPoint in script.points) limitPoint.point[axis] = -limitPoint.point[axis];
			Array.Reverse(script.points);
			script.BuildReachCones();
		}
		
		private void RotatePoints(float degrees, Vector3 axis) {
			foreach (RotationLimitPolygonal.LimitPoint limitPoint in script.points) limitPoint.point = Quaternion.AngleAxis(degrees, axis) * limitPoint.point;
			script.BuildReachCones();
		}
		
		/*
		 * Converting directions from local space to world space
		 * */
		private Vector3 Direction(Vector3 v) {
			if (script.transform.parent == null) return script.defaultLocalRotation * v;
			return script.transform.parent.rotation * (script.defaultLocalRotation * v);
		}
		
		/*
		 * Inverse of Direction(Vector3 v)
		 * */
		private Vector3 InverseDirection(Vector3 v) {
			if (script.transform.parent == null) return Quaternion.Inverse(script.defaultLocalRotation) * v;
			return Quaternion.Inverse(script.defaultLocalRotation) * Quaternion.Inverse(script.transform.parent.rotation) * v;
		}

		/*
		 * Removing Limit Points
		 * */
		private void DeletePoint(int p) {
			RotationLimitPolygonal.LimitPoint[] newPoints = new RotationLimitPolygonal.LimitPoint[0];
				
			for (int i = 0; i < script.points.Length; i++) {
				if (i != p) {
					Array.Resize(ref newPoints, newPoints.Length + 1);
					newPoints[newPoints.Length - 1] = script.points[i];
				}
			}
				
			script.points = newPoints;
			script.BuildReachCones();
		}
		
		/*
		 * Creating new Limit Points
		 * */
		private void AddPoint(int p) {
			RotationLimitPolygonal.LimitPoint[] newPoints = new RotationLimitPolygonal.LimitPoint[script.points.Length + 1];
				
			for (int i = 0; i < p + 1; i++) newPoints[i] = script.points[i];
				
			newPoints[p + 1] = new RotationLimitPolygonal.LimitPoint();
				
			Vector3 nextPoint = Vector3.forward;
			if (p < script.points.Length - 1) nextPoint = script.points[p + 1].point;
			else nextPoint = script.points[0].point;
					
			newPoints[p + 1].point = Vector3.Lerp(script.points[p].point, nextPoint, 0.5f);
				
			for (int i = p + 2; i < newPoints.Length; i++) newPoints[i] = script.points[i - 1];
				
			script.points = newPoints;
			script.BuildReachCones();
		}
		
		/*
		 * Clone properties from another RotationLimitPolygonal
		 * */
		private void CloneLimit() {
			if (clone == null) return;
			if (clone == script) {
				script.LogWarning("Can't clone from self.");
				return;
			}
			
			script.axis = clone.axis;
			script.twistLimit = clone.twistLimit;
			script.smoothIterations = clone.smoothIterations;
			script.points = new RotationLimitPolygonal.LimitPoint[clone.points.Length];
			for (int i = 0; i < script.points.Length; i++) {
				script.points[i] = (RotationLimitPolygonal.LimitPoint)CloneObject(clone.points[i]);
			}
			script.BuildReachCones();
		}
		
		private static object CloneObject(object o) {
			Type t = o.GetType();
			object clone = Activator.CreateInstance(t);
			foreach (FieldInfo fi in t.GetFields()) {
				fi.SetValue(clone, fi.GetValue(o));
			}
			return clone;
		}
		
		/*
		 * Flipping vectors for symmetry
		 * */
		private static Vector3 Symmetrize(Vector3 v, Symmetry symmetry) {
			switch(symmetry) {
			case Symmetry.X: return new Vector3(-v.x, v.y, v.z);
			case Symmetry.Y: return new Vector3(v.x, -v.y, v.z);
			case Symmetry.Z: return new Vector3(v.x, v.y, -v.z);
			default: return v;
			}
		}
		
		/*
		 * Returns closest point to a position. Used for symmetric editing
		 * */
		private RotationLimitPolygonal.LimitPoint GetClosestPoint(Vector3 v) {
			float closestDistace = Mathf.Infinity;
			RotationLimitPolygonal.LimitPoint closestPoint = null;
			
			foreach (RotationLimitPolygonal.LimitPoint limitPoint in script.points) {			
				if (limitPoint.point == v) return limitPoint;
				
				float d = Vector3.Distance(limitPoint.point, v);
				if (d < closestDistace) {
					closestPoint = limitPoint;
					closestDistace = d;
				}
			}
			
			return closestPoint;
		}
		
		#endregion Scene
	}
}