using UnityEngine;
using System.Collections;

namespace Dreamteck.Splines
{
    [ExecuteInEditMode]
    [AddComponentMenu("Dreamteck/Splines/Node Connector")]
    public class Node : MonoBehaviour
    {
        [System.Serializable]
        public class Connection
        {
            public SplineComputer spline
            {
                get { return _computer; }
            }

            public int pointIndex
            {
                get { return _pointIndex; }
            }

            public bool invertTangents = false;

            [SerializeField]
            private int _pointIndex = 0;
            [SerializeField]
            private SplineComputer _computer = null;
            [SerializeField]
            [HideInInspector]
            internal SplinePoint point;

            internal bool isValid
            {
                get
                {
                    if (_computer == null) return false;
                    if (_pointIndex >= _computer.pointCount) return false;
                    return true;
                }
            }

            internal Connection(SplineComputer comp, int index, SplinePoint inputPoint)
            {
                _pointIndex = index;
                _computer = comp;
                point = inputPoint;
            }
        }
        public enum Type { Smooth, Free }
        [HideInInspector]
        public Type type = Type.Smooth;

        public bool transformNormals
        {
            get { return _transformNormals; }
            set
            {
                if (value != _transformNormals)
                {
                    _transformNormals = value;
                    UpdatePoints();
                }
            }
        }

        public bool transformSize
        {
            get { return _transformSize; }
            set
            {
                if (value != _transformSize)
                {
                    _transformSize = value;
                    UpdatePoints();
                }
            }
        }

        public bool transformTangents
        {
            get { return _transformTangents; }
            set
            {
                if (value != _transformTangents)
                {
                    _transformTangents = value;
                    UpdatePoints();
                }
            }
        }

        [SerializeField]
        [HideInInspector]
        protected Connection[] connections = new Connection[0];
        [SerializeField]
        [HideInInspector]
        private bool _transformSize = true;
        [SerializeField]
        [HideInInspector]
        private bool _transformNormals = true;
        [SerializeField]
        [HideInInspector]
        private bool _transformTangents = true;

        private Vector3 _lastPosition, _lastScale;
        private Quaternion _lastRotation;
        private Transform _trs;

        private void Awake()
        {
            _trs = transform;
            SampleTransform();
        }


        void LateUpdate()
        {
            Run();
        }

        void Update()
        {
            Run();
        }

        bool TransformChanged()
        {
#if UNITY_EDITOR
            if(_trs == null) return _lastPosition != transform.position || _lastRotation != transform.rotation || _lastScale != transform.lossyScale;
#endif
            return _lastPosition != _trs.position || _lastRotation != _trs.rotation || _lastScale != _trs.lossyScale;
        }

        void SampleTransform() {
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                _lastPosition = transform.position;
                _lastScale = transform.lossyScale;
                _lastRotation = transform.rotation;
            } 
            else
            {
                _lastPosition = _trs.position;
                _lastScale = _trs.lossyScale;
                _lastRotation = _trs.rotation;
            }
            return;
#else
            _lastPosition = _trs.position;
            _lastScale = _trs.lossyScale;
            _lastRotation = _trs.rotation;
#endif
        }

        private void Run()
        {
            if (TransformChanged())
            {
#if UNITY_EDITOR
                if (!Application.isPlaying)
                {
                    UnityEditor.EditorUtility.SetDirty(this);
                    for (int i = 0; i < connections.Length; i++)
                    {
                        UnityEditor.EditorUtility.SetDirty(connections[i].spline);
                    }
                }
#endif
                UpdateConnectedComputers();
                SampleTransform();
            }
        }

        public SplinePoint GetPoint(int connectionIndex, bool swapTangents)
        {
            SplinePoint point = PointToWorld(connections[connectionIndex].point);
            if (connections[connectionIndex].invertTangents && swapTangents)
            {
                Vector3 tempTan = point.tangent;
                point.tangent = point.tangent2;
                point.tangent2 = tempTan;
            }
            return point;
        }

        public void SetPoint(int connectionIndex, SplinePoint worldPoint, bool swappedTangents)
        {
            Connection connection = connections[connectionIndex];
            connection.point = PointToLocal(worldPoint);
            if (connection.invertTangents && swappedTangents)
            {
                Vector3 tempTan = connection.point.tangent;
                connection.point.tangent = connection.point.tangent2;
                connection.point.tangent2 = tempTan;
            }
            if (type == Type.Smooth)
            {
                if (connection.point.type == SplinePoint.Type.SmoothFree)
                {
                    for (int i = 0; i < connections.Length; i++)
                    {
                        if (i == connectionIndex) continue;
                        Vector3 tanDir = (connection.point.tangent - connection.point.position).normalized;
                        if (tanDir == Vector3.zero) tanDir = -(connection.point.tangent2 - connection.point.position).normalized;
                        float tan1Length = (connections[i].point.tangent - connections[i].point.position).magnitude;
                        float tan2Length = (connections[i].point.tangent2 - connections[i].point.position).magnitude;
                        connections[i].point = connection.point;
                        connections[i].point.tangent = connections[i].point.position + tanDir * tan1Length;
                        connections[i].point.tangent2 = connections[i].point.position - tanDir * tan2Length;
                    }
                }
                else
                {
                    for (int i = 0; i < connections.Length; i++)
                    {
                        if (i == connectionIndex) continue;
                        connections[i].point = connection.point;
                    }
                }
            }
        }

        void OnDestroy()
        {
            ClearConnections();
        }

        public void ClearConnections()
        {
            for (int i = connections.Length-1; i >= 0; i--)
            {
                if (connections[i].spline != null) connections[i].spline.DisconnectNode(connections[i].pointIndex);
            }
            connections = new Connection[0];
        }

        public void UpdateConnectedComputers(SplineComputer excludeComputer = null)
        {
            for (int i = connections.Length - 1; i >= 0; i--)
            {
                if (!connections[i].isValid)
                {
                    RemoveConnection(i);
                    continue;
                }

                if (connections[i].spline == excludeComputer) continue;

                if (type == Type.Smooth && i != 0)
                {
                    SetPoint(i, GetPoint(0, false), false);
                }
                SplinePoint point = GetPoint(i, true);
                if (!transformNormals)
                {
                    point.normal = connections[i].spline.GetPointNormal(connections[i].pointIndex);
                }
                if (!transformTangents)
                {
                    point.tangent = connections[i].spline.GetPointTangent(connections[i].pointIndex);
                    point.tangent2 = connections[i].spline.GetPointTangent2(connections[i].pointIndex);
                }
                if (!transformSize)
                {
                    point.size = connections[i].spline.GetPointSize(connections[i].pointIndex);
                }
                connections[i].spline.SetPoint(connections[i].pointIndex, point);
            }
        }

        public void UpdatePoint(SplineComputer computer, int pointIndex, SplinePoint point, bool updatePosition = true)
        {
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                transform.position = point.position;
            }
            else
            {
                _trs.position = point.position;
            }
#else
            _trs.position = point.position;
#endif
            for (int i = 0; i < connections.Length; i++)
            {
                if (connections[i].spline == computer && connections[i].pointIndex == pointIndex)
                {
                    SetPoint(i, point, true);
                }
            }
        }

        public void UpdatePoints()
        {
            for (int i = connections.Length - 1; i >= 0; i--)
            {
                if (!connections[i].isValid)
                {
                    RemoveConnection(i);
                    continue;
                }
                SplinePoint point = connections[i].spline.GetPoint(connections[i].pointIndex);
                point.SetPosition(transform.position);
                SetPoint(i, point, true);
            }
        }

#if UNITY_EDITOR
        //Use this to maintain the connections between computers in the editor
        public void EditorMaintainConnections()
        {
            RemoveInvalidConnections();
        }
#endif
        //Remove invalid connections
        protected void RemoveInvalidConnections()
        {
            for (int i = connections.Length - 1; i >= 0; i--)
            {
                if (connections[i] == null || !connections[i].isValid) RemoveConnection(i);
            }
        }

        public virtual void AddConnection(SplineComputer computer, int pointIndex)
        {
            RemoveInvalidConnections();
            Node connected = computer.GetNode(pointIndex);
            if (connected != null)
            {
                Debug.LogError(computer.name + " is already connected to node " + connected.name + " at point " + pointIndex);
                return;
            }
            SplinePoint point = computer.GetPoint(pointIndex);
            point.SetPosition(transform.position);
            ArrayUtility.Add(ref connections, new Connection(computer, pointIndex, PointToLocal(point)));
            if(connections.Length == 1) SetPoint(connections.Length - 1, point, true);
            UpdateConnectedComputers();
        }

        protected SplinePoint PointToLocal(SplinePoint worldPoint)
        {
            worldPoint.position = Vector3.zero;
            worldPoint.tangent = transform.InverseTransformPoint(worldPoint.tangent);
            worldPoint.tangent2 = transform.InverseTransformPoint(worldPoint.tangent2);
            worldPoint.normal = transform.InverseTransformDirection(worldPoint.normal);
            worldPoint.size /= (transform.localScale.x + transform.localScale.y + transform.localScale.z)/ 3f;
            return worldPoint;
        }

        protected SplinePoint PointToWorld(SplinePoint localPoint)
        {
            localPoint.position = transform.position;
            localPoint.tangent = transform.TransformPoint(localPoint.tangent);
            localPoint.tangent2 = transform.TransformPoint(localPoint.tangent2);
            localPoint.normal = transform.TransformDirection(localPoint.normal);
            localPoint.size *= (transform.localScale.x + transform.localScale.y + transform.localScale.z) / 3f;
            return localPoint;
        }

        public virtual void RemoveConnection(SplineComputer computer, int pointIndex)
        {
            int index = -1;
            for (int i = 0; i < connections.Length; i++)
            {
                if (connections[i].pointIndex == pointIndex && connections[i].spline == computer)
                {
                    index = i;
                    break;
                }
            }
            if (index < 0) return;
            RemoveConnection(index);
        }

        private void RemoveConnection(int index)
        {
            Connection[] newConnections = new Connection[connections.Length - 1];
            SplineComputer spline = connections[index].spline;
            int pointIndex = connections[index].pointIndex;
            for (int i = 0; i < connections.Length; i++)
            {
                if (i < index) newConnections[i] = connections[i];
                else if (i == index) continue;
                else newConnections[i - 1] = connections[i];
            }
            connections = newConnections;
        }

        public virtual bool HasConnection(SplineComputer computer, int pointIndex)
        {
            for (int i = connections.Length - 1; i >= 0; i--)
            {
                if (!connections[i].isValid)
                {
                    RemoveConnection(i);
                    continue;
                }
                if (connections[i].spline == computer && connections[i].pointIndex == pointIndex) return true;
            }
            return false;
        }

        public Connection[] GetConnections()
        {
            return connections;
        }

    }
}