using System;
using System.Collections;

namespace ProtoBuf.Meta
{
    internal sealed class MutableList : BasicList
    {
        /*  Like BasicList, but allows existing values to be changed
         */
        public new object this[int index]
        {
            get { return head[index]; }
            set { head[index] = value; }
        }
        public void RemoveLast()
        {
            head.RemoveLastWithMutate();
        }

        public void Clear()
        {
            head.Clear();
        }
    }

    internal class BasicList : IEnumerable
    {
        /* Requirements:
         *   - Fast access by index
         *   - Immutable in the tail, so a node can be read (iterated) without locking
         *   - Lock-free tail handling must match the memory mode; struct for Node
         *     wouldn't work as "read" would not be atomic
         *   - Only operation required is append, but this shouldn't go out of its
         *     way to be inefficient
         *   - Assume that the caller is handling thread-safety (to co-ordinate with
         *     other code); no attempt to be thread-safe
         *   - Assume that the data is private; internal data structure is allowed to
         *     be mutable (i.e. array is fine as long as we don't screw it up)
         */
        private static readonly Node nil = new Node(null, 0);

        public void CopyTo(Array array, int offset)
        {
            head.CopyTo(array, offset);
        }

        protected Node head = nil;

        public int Add(object value)
        {
            return (head = head.Append(value)).Length - 1;
        }

        public object this[int index] => head[index];

        //public object TryGet(int index)
        //{
        //    return head.TryGet(index);
        //}

        public void Trim() { head = head.Trim(); }

        public int Count => head.Length;

        IEnumerator IEnumerable.GetEnumerator() => new NodeEnumerator(head);

        public NodeEnumerator GetEnumerator() => new NodeEnumerator(head);

        public struct NodeEnumerator : IEnumerator
        {
            private int position;
            private readonly Node node;
            internal NodeEnumerator(Node node)
            {
                this.position = -1;
                this.node = node;
            }
            void IEnumerator.Reset() { position = -1; }
            public object Current { get { return node[position]; } }
            public bool MoveNext()
            {
                int len = node.Length;
                return (position <= len) && (++position < len);
            }
        }

        internal sealed class Node
        {
            public object this[int index]
            {
                get
                {
                    if (index >= 0 && index < length)
                    {
                        return data[index];
                    }
                    throw new ArgumentOutOfRangeException(nameof(index));
                }
                set
                {
                    if (index >= 0 && index < length)
                    {
                        data[index] = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException(nameof(index));
                    }
                }
            }
            //public object TryGet(int index)
            //{
            //    return (index >= 0 && index < length) ? data[index] : null;
            //}
            private readonly object[] data;

            private int length;
            public int Length => length;

            internal Node(object[] data, int length)
            {
                Helpers.DebugAssert((data == null && length == 0) ||
                    (data != null && length > 0 && length <= data.Length));
                this.data = data;

                this.length = length;
            }

            public void RemoveLastWithMutate()
            {
                if (length == 0) throw new InvalidOperationException();
                length -= 1;
            }

            public Node Append(object value)
            {
                object[] newData;
                int newLength = length + 1;
                if (data == null)
                {
                    newData = new object[10];
                }
                else if (length == data.Length)
                {
                    newData = new object[data.Length * 2];
                    Array.Copy(data, newData, length);
                }
                else
                {
                    newData = data;
                }
                newData[length] = value;
                return new Node(newData, newLength);
            }

            public Node Trim()
            {
                if (length == 0 || length == data.Length) return this;
                object[] newData = new object[length];
                Array.Copy(data, newData, length);
                return new Node(newData, length);
            }

            internal int IndexOfString(string value)
            {
                for (int i = 0; i < length; i++)
                {
                    if ((string)value == (string)data[i]) return i;
                }
                return -1;
            }

            internal int IndexOfReference(object instance)
            {
                for (int i = 0; i < length; i++)
                {
                    if ((object)instance == (object)data[i]) return i;
                } // ^^^ (object) above should be preserved, even if this was typed; needs
                  // to be a reference check
                return -1;
            }

            internal int IndexOf(MatchPredicate predicate, object ctx)
            {
                for (int i = 0; i < length; i++)
                {
                    if (predicate(data[i], ctx)) return i;
                }
                return -1;
            }

            internal void CopyTo(Array array, int offset)
            {
                if (length > 0)
                {
                    Array.Copy(data, 0, array, offset, length);
                }
            }

            internal void Clear()
            {
                if (data != null)
                {
                    Array.Clear(data, 0, data.Length);
                }
                length = 0;
            }
        }

        internal int IndexOf(MatchPredicate predicate, object ctx)
        {
            return head.IndexOf(predicate, ctx);
        }

        internal int IndexOfString(string value)
        {
            return head.IndexOfString(value);
        }

        internal int IndexOfReference(object instance)
        {
            return head.IndexOfReference(instance);
        }

        internal delegate bool MatchPredicate(object value, object ctx);

        internal bool Contains(object value)
        {
            foreach (object obj in this)
            {
                if (object.Equals(obj, value)) return true;
            }
            return false;
        }

        internal sealed class Group
        {
            public readonly int First;
            public readonly BasicList Items;
            public Group(int first)
            {
                this.First = first;
                this.Items = new BasicList();
            }
        }

        internal static BasicList GetContiguousGroups(int[] keys, object[] values)
        {
            if (keys == null) throw new ArgumentNullException(nameof(keys));
            if (values == null) throw new ArgumentNullException(nameof(values));
            if (values.Length < keys.Length) throw new ArgumentException("Not all keys are covered by values", nameof(values));
            BasicList outer = new BasicList();
            Group group = null;
            for (int i = 0; i < keys.Length; i++)
            {
                if (i == 0 || keys[i] != keys[i - 1]) { group = null; }
                if (group == null)
                {
                    group = new Group(keys[i]);
                    outer.Add(group);
                }
                group.Items.Add(values[i]);
            }
            return outer;
        }
    }
}