using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using Unity.Mathematics;

namespace UnityEngine.Splines
{
    /// <summary>
    /// The Spline class is a collection of <see cref="BezierKnot"/>, the closed/open state, and editing representation.
    /// </summary>
    [Serializable]
    public class Spline : ISpline, IList<BezierKnot>
    {
        const int k_CurveDistanceLutResolution = 30;

        [SerializeField]
        SplineType m_EditModeType = SplineType.Bezier;

        [SerializeField]
        List<BezierKnot> m_Knots = new List<BezierKnot>();

        [SerializeField]
        List<DistanceToInterpolation[]> m_LengthsLookupTable = new List<DistanceToInterpolation[]>();

        [SerializeField]
        float m_Length = -1f;

        [SerializeField]
        bool m_Closed;

        /// <summary>
        /// Return the number of knots.
        /// </summary>
        public int Count => m_Knots.Count;

        /// <summary>
        /// Returns true if this Spline is read-only, false if it is mutable.
        /// </summary>
        public bool IsReadOnly => false;

        /// <summary>
        /// Invoked in the editor any time a spline property is modified.
        /// </summary>
        /// <remarks>
        /// In the editor this can be invoked many times per-frame.
        /// Prefer to use <see cref="UnityEditor.Splines.EditorSplineUtility.afterSplineDataWasModified"/> when
        /// working with splines in the editor.
        /// </remarks>
        public event Action changed;

#if UNITY_EDITOR
        internal static Action<Spline> afterSplineWasModified;
        [NonSerialized] //In the editor, this seemed to be surviving domain reloads
        bool m_Dirty;
#endif

        internal void SetDirty()
        {
            SetLengthCacheDirty();
            changed?.Invoke();
            OnSplineChanged();

#if UNITY_EDITOR
            if (m_Dirty)
                return;

            m_Dirty = true;

            UnityEditor.EditorApplication.delayCall += () =>
            {
                afterSplineWasModified?.Invoke(this);
                m_Dirty = false;
            };
#endif
        }

        /// <summary>
        /// Invoked any time a spline property is modified.
        /// </summary>
        /// <remarks>
        /// In the editor this can be invoked many times per-frame.
        /// Prefer to use <see cref="UnityEditor.Splines.EditorSplineUtility.afterSplineWasModified"/> when working
        /// with splines in the editor.
        /// </remarks>
        protected virtual void OnSplineChanged()
        {
        }

        // todo Remove this and refactor m_Knots to store a struct with knot+cached data
        void EnsureCurveLengthCacheValid()
        {
            if (m_LengthsLookupTable.Count != m_Knots.Count)
            {
                m_LengthsLookupTable.Clear();
                m_LengthsLookupTable.Capacity = m_Knots.Count;
                for (int i = 0; i < m_Knots.Count; i++)
                    m_LengthsLookupTable.Add(null);
            }
        }

        // todo Only Catmull Rom requires every curve to be re-evaluated when dirty.
        // Linear and cubic bezier could be more selective about dirtying cached curve lengths.
        // Important - This function also serves to enable backwards compatibility with serialized Spline instances
        // that did not have a length cache.
        void SetLengthCacheDirty()
        {
            EnsureCurveLengthCacheValid();
            m_Length = -1f;
            for (int i = 0; i < m_Knots.Count; i++)
                m_LengthsLookupTable[i] = null;
        }

        /// <summary>
        /// The SplineType that this spline should be presented as to the user.
        /// </summary>
        /// <remarks>
        /// Internally all splines are stored as a collection of bezier knots, and when editing converted or displayed
        /// with the handles appropriate to the editable type.
        /// </remarks>
        public SplineType EditType
        {
            get => m_EditModeType;
            set
            {
                if (m_EditModeType == value)
                    return;

                m_EditModeType = value;
                SetDirty();
            }
        }

        /// <summary>
        /// A collection of <see cref="BezierKnot"/>.
        /// </summary>
        public IEnumerable<BezierKnot> Knots => m_Knots;

        /// <summary>
        /// Whether the spline is open (has a start and end point) or closed (forms an unbroken loop).
        /// </summary>
        public bool Closed
        {
            get => m_Closed;
            set
            {
                if (m_Closed == value)
                    return;
                m_Closed = value;
                SetDirty();
            }
        }

        /// <summary>
        /// Return the first index of an element matching item.
        /// </summary>
        /// <param name="item">The knot to locate.</param>
        /// <returns>The zero-based index of the knot, or -1 if not found.</returns>
        public int IndexOf(BezierKnot item) => m_Knots.IndexOf(item);

        /// <summary>
        /// Insert a <see cref="BezierKnot"/> at the specified <paramref name="index"/>.
        /// </summary>
        /// <param name="index">The zero-based index to insert the new element.</param>
        /// <param name="knot">The <see cref="BezierKnot"/> to insert.</param>
        public void Insert(int index, BezierKnot knot)
        {
            m_Knots.Insert(index, knot);
            m_LengthsLookupTable.Insert(index, null);
            SetDirty();
        }

        /// <summary>
        /// Removes the knot at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index of the element to remove.</param>
        public void RemoveAt(int index)
        {
            m_Knots.RemoveAt(index);
            m_LengthsLookupTable.RemoveAt(index);
            SetDirty();
        }

        /// <summary>
        /// Get or set the knot at <paramref name="index"/>.
        /// </summary>
        /// <param name="index">The zero-based index of the element to get or set.</param>
        public BezierKnot this[int index]
        {
            get => m_Knots[index];
            set
            {
                m_Knots[index] = value;
                SetDirty();
            }
        }

        /// <summary>
        /// Default constructor creates a spline with no knots, not closed.
        /// </summary>
        public Spline() { }

        /// <summary>
        /// Create a spline with a pre-allocated knot capacity.
        /// </summary>
        /// <param name="knotCapacity">The capacity of the knot collection.</param>
        /// <param name="closed">Whether the spline is open (has a start and end point) or closed (forms an unbroken loop).</param>
        public Spline(int knotCapacity, bool closed = false)
        {
            m_Knots = new List<BezierKnot>(knotCapacity);
            m_Closed = closed;
        }

        /// <summary>
        /// Create a spline from a collection of <see cref="BezierKnot"/>.
        /// </summary>
        /// <param name="knots">A collection of <see cref="BezierKnot"/>.</param>
        /// <param name="closed">Whether the spline is open (has a start and end point) or closed (forms an unbroken loop).</param>
        public Spline(IEnumerable<BezierKnot> knots, bool closed = false)
        {
            m_Knots = knots.ToList();
            m_Closed = closed;
            SetDirty();
        }

        /// <summary>
        /// Get a <see cref="BezierCurve"/> from a knot index.
        /// </summary>
        /// <param name="index">The knot index that serves as the first control point for this curve.</param>
        /// <returns>
        /// A <see cref="BezierCurve"/> formed by the knot at index and the next knot.
        /// </returns>
        public BezierCurve GetCurve(int index)
        {
            int next = m_Closed ? (index + 1) % m_Knots.Count : math.min(index + 1, m_Knots.Count - 1);
            return new BezierCurve(m_Knots[index], m_Knots[next]);
        }

        /// <summary>
        /// Return the length of a curve.
        /// </summary>
        /// <param name="index"></param>
        /// <seealso cref="Warmup"/>
        /// <seealso cref="GetLength"/>
        /// <returns></returns>
        public float GetCurveLength(int index)
        {
            EnsureCurveLengthCacheValid();
            if(m_LengthsLookupTable[index] == null)
            {
                m_LengthsLookupTable[index] = new DistanceToInterpolation[k_CurveDistanceLutResolution];
                CurveUtility.CalculateCurveLengths(GetCurve(index), m_LengthsLookupTable[index]);
            }

            var cumulativeCurveLengths = m_LengthsLookupTable[index];
            return cumulativeCurveLengths.Length > 0 ? cumulativeCurveLengths[cumulativeCurveLengths.Length - 1].Distance : 0f;
        }

        /// <summary>
        /// Return the sum of all curve lengths, accounting for <see cref="Closed"/> state.
        /// Note that this value is not accounting for transform hierarchy. If you require length in world space use
        /// <see cref="SplineUtility.CalculateLength"/>.
        /// </summary>
        /// <remarks>
        /// This value is cached. It is recommended to call this once in a non-performance critical path to ensure that
        /// the cache is valid.
        /// </remarks>
        /// <seealso cref="Warmup"/>
        /// <seealso cref="GetCurveLength"/>
        /// <returns>
        /// Returns the sum length of all curves composing this spline, accounting for closed state.
        /// </returns>
        public float GetLength()
        {
            if (m_Length < 0f)
            {
                m_Length = 0f;
                for (int i = 0, c = Closed ? Count : Count - 1; i < c; ++i)
                    m_Length += GetCurveLength(i);
            }

            return m_Length;
        }

        DistanceToInterpolation[] GetCurveDistanceLut(int index)
        {
            if (m_LengthsLookupTable[index] == null)
            {
                m_LengthsLookupTable[index] = new DistanceToInterpolation[k_CurveDistanceLutResolution];
                CurveUtility.CalculateCurveLengths(GetCurve(index), m_LengthsLookupTable[index]);
            }

            return m_LengthsLookupTable[index];
        }

        /// <summary>
        /// Return the normalized interpolation (t) corresponding to a distance on a <see cref="BezierCurve"/>.
        /// </summary>
        /// <param name="curveIndex"> The zero-based index of the curve.</param>
        /// <param name="curveDistance">The curve-relative distance to convert to an interpolation ratio (also referred to as 't').</param>
        /// <returns>  The normalized interpolation ratio associated to distance on the designated curve.</returns>
        public float GetCurveInterpolation(int curveIndex, float curveDistance)
            => CurveUtility.GetDistanceToInterpolation(GetCurveDistanceLut(curveIndex), curveDistance);

        /// <summary>
        /// Ensure that all caches contain valid data. Call this to avoid unexpected performance costs when accessing
        /// spline data. Caches remain valid until any part of the spline state is modified.
        /// </summary>
        public void Warmup()
        {
            var _ = GetLength();
        }

        /// <summary>
        /// Change the size of the <see cref="BezierKnot"/> list.
        /// </summary>
        /// <param name="newSize">The new size of the knots collection.</param>
        public void Resize(int newSize)
        {
            int count = Count;
            if (newSize == count)
                return;

            if (newSize > count)
            {
                while (m_Knots.Count < newSize)
                {
                    m_Knots.Add(new BezierKnot { Rotation = quaternion.identity });
                    m_LengthsLookupTable.Add(null);
                }
            }
            else if (newSize < count)
            {
                m_Knots.RemoveRange(newSize, m_Knots.Count - newSize);
                m_LengthsLookupTable.RemoveRange(newSize, m_Knots.Count - newSize);
            }

            SetDirty();
        }

        /// <summary>
        /// Create an array of spline knots.
        /// </summary>
        /// <returns>Return a new array copy of the knots collection.</returns>
        public BezierKnot[] ToArray()
        {
            return m_Knots.ToArray();
        }

        /// <summary>
        /// Copy the values from <paramref name="toCopy"/> to this spline.
        /// </summary>
        /// <param name="toCopy">The Spline to copy property data from.</param>
        public void Copy(Spline toCopy)
        {
            if (toCopy == this)
                return;

            m_EditModeType = toCopy.m_EditModeType;
            m_Closed = toCopy.Closed;
            m_Knots.Clear();
            m_Knots.AddRange(toCopy.m_Knots);
            m_LengthsLookupTable.AddRange(toCopy.m_LengthsLookupTable);
            SetDirty();
        }

        /// <summary>
        /// Get an enumerator that iterates through the <see cref="BezierKnot"/> collection.
        /// </summary>
        /// <returns>An IEnumerator that is used to iterate the <see cref="BezierKnot"/> collection.</returns>
        public IEnumerator<BezierKnot> GetEnumerator() => m_Knots.GetEnumerator();

        /// <inheritdoc cref="GetEnumerator"/>
        IEnumerator IEnumerable.GetEnumerator() => m_Knots.GetEnumerator();

        /// <summary>
        /// Adds a knot to the spline.
        /// </summary>
        /// <param name="item">The <see cref="BezierKnot"/> to add.</param>
        public void Add(BezierKnot item)
        {
            m_Knots.Add(item);
            SetDirty();
        }

        /// <summary>
        /// Remove all knots from the spline.
        /// </summary>
        public void Clear()
        {
            m_Knots.Clear();
            SetDirty();
        }

        /// <summary>
        /// Return true if a knot is present in the spline.
        /// </summary>
        /// <param name="item">The <see cref="BezierKnot"/> to locate.</param>
        /// <returns>Returns true if the knot is found, false if it is not present.</returns>
        public bool Contains(BezierKnot item) => m_Knots.Contains(item);

        /// <summary>
        /// Copies the contents of the knot list to an array starting at an index.
        /// </summary>
        /// <param name="array">The destination array to place the copied item in.</param>
        /// <param name="arrayIndex">The zero-based index to copy.</param>
        public void CopyTo(BezierKnot[] array, int arrayIndex) => m_Knots.CopyTo(array, arrayIndex);

        /// <summary>
        /// Removes the first matching knot.
        /// </summary>
        /// <param name="item">The <see cref="BezierKnot"/> to locate and remove.</param>
        /// <returns>Returns true if a matching item was found and removed, false if no match was discovered.</returns>
        public bool Remove(BezierKnot item)
        {
            if (m_Knots.Remove(item))
            {
                SetDirty();
                return true;
            }

            return false;
        }
    }
}