using System;
using System.Collections.Generic;

namespace UnityEngine.U2D
{
    // Spline Internal Meta Data.
    internal struct SplinePointMetaData
    {
        public float height;
        public uint spriteIndex;
        public int cornerMode;
    };    
    
    /// <summary>
    /// Spline contains control points used to define curve/outline for generating SpriteShape geometry.
    /// </summary>
    [Serializable]
    public class Spline
    {
        private static readonly string KErrorMessage = "Internal error: Point too close to neighbor";
        private static readonly float KEpsilon = 0.01f;
        [SerializeField]
        private bool m_IsOpenEnded;
        [SerializeField]
        private List<SplineControlPoint> m_ControlPoints = new List<SplineControlPoint>();

        /// <summary>
        /// Get/Set Spline's shape to open ended or closed.
        /// </summary>
        public bool isOpenEnded
        {
            get
            {
                if (GetPointCount() < 3)
                    return true;

                return m_IsOpenEnded;
            }
            set { m_IsOpenEnded = value; }
        }

        private bool IsPositionValid(int index, int next, Vector3 point)
        {
            int pointCount = GetPointCount();
            if (isOpenEnded && (index == 0 || index == pointCount))
                return true;
            
            int prev = (index == 0) ? (pointCount - 1) : (index - 1);
            if (prev >= 0)
            {
                Vector3 diff = m_ControlPoints[prev].position - point;
                if (diff.magnitude < KEpsilon)
                    return false;
            }
            next = (next >= pointCount) ? 0 : next;
            if (next < pointCount)
            {
                Vector3 diff = m_ControlPoints[next].position - point;
                if (diff.magnitude < KEpsilon)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Clear all control points.
        /// </summary>
        public void Clear()
        {
            m_ControlPoints.Clear();
        }

        /// <summary>
        /// Get Spline's control point count.
        /// </summary>
        /// <returns>Count of control points.</returns>
        public int GetPointCount()
        {
            return m_ControlPoints.Count;
        }

        /// <summary>
        /// Insert control point at index.
        /// </summary>
        /// <param name="index">Index at which a control point will be inserted.</param>
        /// <param name="point">Position of the control point.</param>
        /// <exception cref="ArgumentException"></exception>
        public void InsertPointAt(int index, Vector3 point)
        {
            if (!IsPositionValid(index, index, point))
                throw new ArgumentException(KErrorMessage);
            m_ControlPoints.Insert(index, new SplineControlPoint { position = point, height = 1.0f, cornerMode = Corner.Automatic });
        }

        /// <summary>
        /// Remove a control point from the Spline at index.
        /// </summary>
        /// <param name="index">Index of the control point to be removed.</param>
        public void RemovePointAt(int index)
        {
            if (m_ControlPoints.Count > 2)
                m_ControlPoints.RemoveAt(index);
        }

        /// <summary>
        /// Get position of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns></returns>
        public Vector3 GetPosition(int index)
        {
            return m_ControlPoints[index].position;
        }

        /// <summary>
        /// Set position of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="point">Position of control point.</param>
        /// <exception cref="ArgumentException"></exception>
        public void SetPosition(int index, Vector3 point)
        {
            if (!IsPositionValid(index, index + 1, point))
                throw new ArgumentException(KErrorMessage);
            SplineControlPoint newPoint = m_ControlPoints[index];
            newPoint.position = point;
            m_ControlPoints[index] = newPoint;
        }

        /// <summary>
        /// Get left tangent of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns>Left tangent of control point.</returns>
        public Vector3 GetLeftTangent(int index)
        {
            ShapeTangentMode mode = GetTangentMode(index);

            if (mode == ShapeTangentMode.Linear)
                return Vector3.zero;

            return m_ControlPoints[index].leftTangent;
        }

        /// <summary>
        /// Set left tangent of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="tangent">Left tangent of control point.</param>
        public void SetLeftTangent(int index, Vector3 tangent)
        {
            ShapeTangentMode mode = GetTangentMode(index);

            if (mode == ShapeTangentMode.Linear)
                return;

            SplineControlPoint newPoint = m_ControlPoints[index];
            newPoint.leftTangent = tangent;
            m_ControlPoints[index] = newPoint;
        }

        /// <summary>
        /// Get right tangent of control point at index,
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns>Right tangent of control point.</returns>
        public Vector3 GetRightTangent(int index)
        {
            ShapeTangentMode mode = GetTangentMode(index);

            if (mode == ShapeTangentMode.Linear)
                return Vector3.zero;

            return m_ControlPoints[index].rightTangent;
        }

        /// <summary>
        /// Set right tangent of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="tangent">Right tangent of control point.</param>
        public void SetRightTangent(int index, Vector3 tangent)
        {
            ShapeTangentMode mode = GetTangentMode(index);

            if (mode == ShapeTangentMode.Linear)
                return;

            SplineControlPoint newPoint = m_ControlPoints[index];
            newPoint.rightTangent = tangent;
            m_ControlPoints[index] = newPoint;
        }

        /// <summary>
        /// Get tangent mode of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns>Tangent mode of control point</returns>
        public ShapeTangentMode GetTangentMode(int index)
        {
            return m_ControlPoints[index].mode;
        }

        /// <summary>
        /// Set the tangent mode of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="mode">Tangent mode.</param>
        public void SetTangentMode(int index, ShapeTangentMode mode)
        {
            SplineControlPoint newPoint = m_ControlPoints[index];
            newPoint.mode = mode;
            m_ControlPoints[index] = newPoint;
        }

        /// <summary>
        /// Get height of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns>Height.</returns>
        public float GetHeight(int index)
        {
            return m_ControlPoints[index].height;
        }

        /// <summary>
        /// Set height of control point at index.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="value">Height.</param>
        public void SetHeight(int index, float value)
        {
            m_ControlPoints[index].height = value;
        }

        /// <summary>
        /// Get Sprite index to be used for rendering edge starting at control point.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns>Sprite index.</returns>
        public int GetSpriteIndex(int index)
        {
            return m_ControlPoints[index].spriteIndex;
        }

        /// <summary>
        /// Set Sprite index to be used for rendering edge starting at control point.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="value">Sprite index.</param>
        public void SetSpriteIndex(int index, int value)
        {
            m_ControlPoints[index].spriteIndex = value;
        }

        /// <summary>
        /// Test if a corner mode is enabled at control point.  
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <returns>True if a valid corner mode is set.</returns>
        public bool GetCorner(int index)
        {
            return GetCornerMode(index) != Corner.Disable;
        }

        /// <summary>
        /// Set corner mode to automatic or disabled.
        /// </summary>
        /// <param name="index">Index of control point.</param>
        /// <param name="value">Enable/disable corner mode</param>
        public void SetCorner(int index, bool value)
        {
            m_ControlPoints[index].corner = value;
            m_ControlPoints[index].cornerMode = value ? Corner.Automatic : Corner.Disable;
        }

        internal void SetCornerMode(int index, Corner value)
        {
            m_ControlPoints[index].corner = (value != Corner.Disable);
            m_ControlPoints[index].cornerMode = value;
        }
        
        internal Corner GetCornerMode(int index)
        {
            if (m_ControlPoints[index].cornerMode == Corner.Disable)
            {
                // For backward compatibility.
                if (m_ControlPoints[index].corner)
                {
                    m_ControlPoints[index].cornerMode = Corner.Automatic;
                    return Corner.Automatic;
                }
            }
            return m_ControlPoints[index].cornerMode;
        }

        /// <summary>
        /// Get hash code for this Spline to test for changes.
        /// </summary>
        /// <returns>Hash code as int.</returns>
        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = (int)2166136261;

                for (int i = 0; i < GetPointCount(); ++i)
                {
                    hashCode = hashCode * 16777619 ^ m_ControlPoints[i].GetHashCode();
                }

                hashCode = hashCode * 16777619 ^ m_IsOpenEnded.GetHashCode();

                return hashCode;
            }
        }        
    }
}