namespace SRF
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using UnityEngine;

    /// <summary>
    /// IList implementation which does not release the buffer when clearing/removing elements. Based on the NGUI BetterList
    /// </summary>
    [Serializable]
    public class SRList<T> : IList<T>, ISerializationCallbackReceiver
    {
        [SerializeField] private T[] _buffer;
        [SerializeField] private int _count;
        private EqualityComparer<T> _equalityComparer;
        private ReadOnlyCollection<T> _readOnlyWrapper;
        public SRList() {}

        public SRList(int capacity)
        {
            Buffer = new T[capacity];
        }

        /// <summary>
        /// Create a new list with the range of values. Contains a foreach loop, which will allocate garbage when used with most
        /// generic collection types.
        /// </summary>
        public SRList(IEnumerable<T> source)
        {
            AddRange(source);
        }

        public T[] Buffer
        {
            get { return _buffer; }
            private set { _buffer = value; }
        }

        private EqualityComparer<T> EqualityComparer
        {
            get
            {
                if (_equalityComparer == null)
                {
                    _equalityComparer = EqualityComparer<T>.Default;
                }

                return _equalityComparer;
            }
        }

        public int Count
        {
            get { return _count; }
            private set { _count = value; }
        }

        public IEnumerator<T> GetEnumerator()
        {
            if (Buffer != null)
            {
                for (var i = 0; i < Count; ++i)
                {
                    yield return Buffer[i];
                }
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public void Add(T item)
        {
            if (Buffer == null || Count == Buffer.Length)
            {
                Expand();
            }

            Buffer[Count++] = item;
        }

        public void Clear()
        {
            Count = 0;
        }

        public bool Contains(T item)
        {
            if (Buffer == null)
            {
                return false;
            }

            for (var i = 0; i < Count; ++i)
            {
                if (EqualityComparer.Equals(Buffer[i], item))
                {
                    return true;
                }
            }

            return false;
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            Trim();
            Buffer.CopyTo(array, arrayIndex);
        }

        public bool Remove(T item)
        {
            if (Buffer == null)
            {
                return false;
            }

            var index = IndexOf(item);

            if (index < 0)
            {
                return false;
            }

            RemoveAt(index);

            return true;
        }

        public bool IsReadOnly
        {
            get { return false; }
        }

        public int IndexOf(T item)
        {
            if (Buffer == null)
            {
                return -1;
            }

            for (var i = 0; i < Count; ++i)
            {
                if (EqualityComparer.Equals(Buffer[i], item))
                {
                    return i;
                }
            }

            return -1;
        }

        public void Insert(int index, T item)
        {
            if (Buffer == null || Count == Buffer.Length)
            {
                Expand();
            }

            if (index < Count)
            {
                for (var i = Count; i > index; --i)
                {
                    Buffer[i] = Buffer[i - 1];
                }
                Buffer[index] = item;
                ++Count;
            }
            else
            {
                Add(item);
            }
        }

        public void RemoveAt(int index)
        {
            if (Buffer != null && index < Count)
            {
                --Count;
                Buffer[index] = default(T);
                for (var b = index; b < Count; ++b)
                {
                    Buffer[b] = Buffer[b + 1];
                }
            }
        }

        public T this[int index]
        {
            get
            {
                if (Buffer == null)
                {
                    throw new IndexOutOfRangeException();
                }

                return Buffer[index];
            }
            set
            {
                if (Buffer == null)
                {
                    throw new IndexOutOfRangeException();
                }

                Buffer[index] = value;
            }
        }

        public void OnBeforeSerialize()
        {
            // Clean buffer of unused elements before serializing
            Clean();
        }

        public void OnAfterDeserialize()
        {
        }

        /// <summary>
        /// Add range of values to the list. Contains a foreach loop, which will allocate garbage when used with most
        /// generic collection types.
        /// </summary>
        /// <param name="range"></param>
        public void AddRange(IEnumerable<T> range)
        {
            foreach (var item in range)
            {
                Add(item);
            }
        }

        /// <summary>
        /// Clear the list, optionally setting each element to default(T)
        /// </summary>
        public void Clear(bool clean)
        {
            Clear();

            if (!clean)
            {
                return;
            }

            Clean();
        }

        public void Clean()
        {
            if (Buffer == null)
            {
                return;
            }

            for (var i = Count; i < _buffer.Length; i++)
            {
                _buffer[i] = default(T);
            }
        }

        /// <summary>
        /// Get a read-only wrapper of this list. This is cached, so very little cost after first called.
        /// </summary>
        /// <returns></returns>
        public ReadOnlyCollection<T> AsReadOnly()
        {
            if (_readOnlyWrapper == null)
            {
                _readOnlyWrapper = new ReadOnlyCollection<T>(this);
            }

            return _readOnlyWrapper;
        }

        /// <summary>
        /// Helper function that expands the size of the array, maintaining the content.
        /// </summary>
        private void Expand()
        {
            var newList = (Buffer != null) ? new T[Mathf.Max(Buffer.Length << 1, 32)] : new T[32];

            if (Buffer != null && Count > 0)
            {
                Buffer.CopyTo(newList, 0);
            }

            Buffer = newList;
        }

        /// <summary>
        /// Trim the unnecessary memory, resizing the buffer to be of 'Length' size.
        /// Call this function only if you are sure that the buffer won't need to resize anytime soon.
        /// </summary>
        public void Trim()
        {
            if (Count > 0)
            {
                if (Count >= Buffer.Length)
                {
                    return;
                }

                var newList = new T[Count];

                for (var i = 0; i < Count; ++i)
                {
                    newList[i] = Buffer[i];
                }

                Buffer = newList;
            }
            else
            {
                Buffer = new T[0];
            }
        }

        /// <summary>
        /// List.Sort equivalent.
        /// </summary>
        public void Sort(Comparison<T> comparer)
        {
            var changed = true;

            while (changed)
            {
                changed = false;

                for (var i = 1; i < Count; ++i)
                {
                    if (comparer.Invoke(Buffer[i - 1], Buffer[i]) > 0)
                    {
                        var temp = Buffer[i];
                        Buffer[i] = Buffer[i - 1];
                        Buffer[i - 1] = temp;
                        changed = true;
                    }
                }
            }
        }
    }
}
