// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.Language.Syntax
{
    [StructLayout(LayoutKind.Auto)]
    internal readonly struct SyntaxTriviaList : IEquatable<SyntaxTriviaList>, IReadOnlyList<SyntaxTrivia>
    {
        public static SyntaxTriviaList Empty => default(SyntaxTriviaList);

        internal SyntaxTriviaList(SyntaxNode node, int position, int index = 0)
        {
            Node = node;
            Position = position;
            Index = index;
        }

        internal SyntaxTriviaList(SyntaxNode node)
        {
            Node = node;
            Position = node.Position;
            Index = 0;
        }

        public SyntaxTriviaList(SyntaxTrivia trivia)
        {
            Node = trivia;
            Position = 0;
            Index = 0;
        }

        /// <summary>
        /// Creates a list of trivia.
        /// </summary>
        /// <param name="trivias">An array of trivia.</param>
        public SyntaxTriviaList(params SyntaxTrivia[] trivias)
            : this(CreateNode(trivias), 0, 0)
        {
        }

        /// <summary>
        /// Creates a list of trivia.
        /// </summary>
        /// <param name="trivias">A sequence of trivia.</param>
        public SyntaxTriviaList(IEnumerable<SyntaxTrivia> trivias)
            : this(SyntaxTriviaListBuilder.Create(trivias).Node, 0, 0)
        {
        }

        private static SyntaxNode CreateNode(SyntaxTrivia[] trivias)
        {
            if (trivias == null)
            {
                return null;
            }

            var builder = new SyntaxTriviaListBuilder(trivias.Length);
            builder.Add(trivias);
            return builder.ToList().Node;
        }

        internal SyntaxNode Node { get; }

        internal int Position { get; }

        internal int Index { get; }

        public int Count
        {
            get { return Node == null ? 0 : (Node.IsList ? Node.SlotCount : 1); }
        }

        public SyntaxTrivia ElementAt(int index)
        {
            return this[index];
        }

        /// <summary>
        /// Gets the trivia at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index of the trivia to get.</param>
        /// <returns>The token at the specified index.</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index" /> is less than 0.-or-<paramref name="index" /> is equal to or greater than <see cref="Count" />. </exception>
        public SyntaxTrivia this[int index]
        {
            get
            {
                if (Node != null)
                {
                    if (Node.IsList)
                    {
                        if (unchecked((uint)index < (uint)Node.SlotCount))
                        {
                            return Node.GetNodeSlot(index) as SyntaxTrivia;
                        }
                    }
                    else if (index == 0)
                    {
                        return Node as SyntaxTrivia;
                    }
                }

                throw new ArgumentOutOfRangeException(nameof(index));
            }
        }

        /// <summary>
        /// The absolute span of the list elements in characters, including the leading and trailing trivia of the first and last elements.
        /// </summary>
        public TextSpan FullSpan
        {
            get
            {
                if (Node == null)
                {
                    return default(TextSpan);
                }

                return new TextSpan(Position, Node.FullWidth);
            }
        }

        /// <summary>
        /// The absolute span of the list elements in characters, not including the leading and trailing trivia of the first and last elements.
        /// </summary>
        public TextSpan Span
        {
            get
            {
                if (Node == null)
                {
                    return default(TextSpan);
                }

                return TextSpan.FromBounds(Position + Node.Green.GetLeadingTriviaWidth(),
                    Position + Node.FullWidth - Node.Green.GetTrailingTriviaWidth());
            }
        }

        /// <summary>
        /// Returns the first trivia in the list.
        /// </summary>
        /// <returns>The first trivia in the list.</returns>
        /// <exception cref="InvalidOperationException">The list is empty.</exception>
        public SyntaxTrivia First()
        {
            if (Any())
            {
                return this[0];
            }

            throw new InvalidOperationException();
        }

        /// <summary>
        /// Returns the last trivia in the list.
        /// </summary>
        /// <returns>The last trivia in the list.</returns>
        /// <exception cref="InvalidOperationException">The list is empty.</exception>
        public SyntaxTrivia Last()
        {
            if (Any())
            {
                return this[Count - 1];
            }

            throw new InvalidOperationException();
        }

        /// <summary>
        /// Does this list have any items.
        /// </summary>
        public bool Any()
        {
            return Node != null;
        }

        /// <summary>
        /// Returns a list which contains all elements of <see cref="SyntaxTriviaList"/> in reversed order.
        /// </summary>
        /// <returns><see cref="Reversed"/> which contains all elements of <see cref="SyntaxTriviaList"/> in reversed order</returns>
        public Reversed Reverse()
        {
            return new Reversed(this);
        }

        public Enumerator GetEnumerator()
        {
            return new Enumerator(this);
        }

        public int IndexOf(SyntaxTrivia triviaInList)
        {
            for (int i = 0, n = Count; i < n; i++)
            {
                var trivia = this[i];
                if (trivia == triviaInList)
                {
                    return i;
                }
            }

            return -1;
        }

        internal int IndexOf(SyntaxKind kind)
        {
            for (int i = 0, n = Count; i < n; i++)
            {
                if (this[i].Kind == kind)
                {
                    return i;
                }
            }

            return -1;
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified trivia added to the end.
        /// </summary>
        /// <param name="trivia">The trivia to add.</param>
        public SyntaxTriviaList Add(SyntaxTrivia trivia)
        {
            return Insert(Count, trivia);
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified trivia added to the end.
        /// </summary>
        /// <param name="trivia">The trivia to add.</param>
        public SyntaxTriviaList AddRange(IEnumerable<SyntaxTrivia> trivia)
        {
            return InsertRange(Count, trivia);
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified trivia inserted at the index.
        /// </summary>
        /// <param name="index">The index in the list to insert the trivia at.</param>
        /// <param name="trivia">The trivia to insert.</param>
        public SyntaxTriviaList Insert(int index, SyntaxTrivia trivia)
        {
            if (trivia == default(SyntaxTrivia))
            {
                throw new ArgumentOutOfRangeException(nameof(trivia));
            }

            return InsertRange(index, new[] { trivia });
        }

        private static readonly ObjectPool<SyntaxTriviaListBuilder> s_builderPool =
            new ObjectPool<SyntaxTriviaListBuilder>(() => SyntaxTriviaListBuilder.Create());

        private static SyntaxTriviaListBuilder GetBuilder()
            => s_builderPool.Allocate();

        private static void ClearAndFreeBuilder(SyntaxTriviaListBuilder builder)
        {
            // It's possible someone might create a list with a huge amount of trivia
            // in it.  We don't want to hold onto such items forever.  So only cache
            // reasonably sized lists.  In IDE testing, around 99% of all trivia lists
            // were 16 or less elements.
            const int MaxBuilderCount = 16;
            if (builder.Count <= MaxBuilderCount)
            {
                builder.Clear();
                s_builderPool.Free(builder);
            }
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified trivia inserted at the index.
        /// </summary>
        /// <param name="index">The index in the list to insert the trivia at.</param>
        /// <param name="trivia">The trivia to insert.</param>
        public SyntaxTriviaList InsertRange(int index, IEnumerable<SyntaxTrivia> trivia)
        {
            var thisCount = Count;
            if (index < 0 || index > thisCount)
            {
                throw new ArgumentOutOfRangeException(nameof(index));
            }

            if (trivia == null)
            {
                throw new ArgumentNullException(nameof(trivia));
            }

            // Just return ourselves if we're not being asked to add anything.
            if (trivia is ICollection<SyntaxTrivia> triviaCollection && triviaCollection.Count == 0)
            {
                return this;
            }

            var builder = GetBuilder();
            try
            {
                for (var i = 0; i < index; i++)
                {
                    builder.Add(this[i]);
                }

                builder.AddRange(trivia);

                for (var i = index; i < thisCount; i++)
                {
                    builder.Add(this[i]);
                }

                return builder.Count == thisCount ? this : builder.ToList();
            }
            finally
            {
                ClearAndFreeBuilder(builder);
            }
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the element at the specified index removed.
        /// </summary>
        /// <param name="index">The index identifying the element to remove.</param>
        public SyntaxTriviaList RemoveAt(int index)
        {
            if (index < 0 || index >= Count)
            {
                throw new ArgumentOutOfRangeException(nameof(index));
            }

            var list = this.ToList();
            list.RemoveAt(index);
            return new SyntaxTriviaList(list);
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified element removed.
        /// </summary>
        /// <param name="triviaInList">The trivia element to remove.</param>
        public SyntaxTriviaList Remove(SyntaxTrivia triviaInList)
        {
            var index = IndexOf(triviaInList);
            if (index >= 0 && index < Count)
            {
                return RemoveAt(index);
            }

            return this;
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified element replaced with new trivia.
        /// </summary>
        /// <param name="triviaInList">The trivia element to replace.</param>
        /// <param name="newTrivia">The trivia to replace the element with.</param>
        public SyntaxTriviaList Replace(SyntaxTrivia triviaInList, SyntaxTrivia newTrivia)
        {
            if (newTrivia == default(SyntaxTrivia))
            {
                throw new ArgumentOutOfRangeException(nameof(newTrivia));
            }

            return ReplaceRange(triviaInList, new[] { newTrivia });
        }

        /// <summary>
        /// Creates a new <see cref="SyntaxTriviaList"/> with the specified element replaced with new trivia.
        /// </summary>
        /// <param name="triviaInList">The trivia element to replace.</param>
        /// <param name="newTrivia">The trivia to replace the element with.</param>
        public SyntaxTriviaList ReplaceRange(SyntaxTrivia triviaInList, IEnumerable<SyntaxTrivia> newTrivia)
        {
            var index = IndexOf(triviaInList);
            if (index >= 0 && index < Count)
            {
                var list = this.ToList();
                list.RemoveAt(index);
                list.InsertRange(index, newTrivia);
                return new SyntaxTriviaList(list);
            }

            throw new ArgumentOutOfRangeException(nameof(triviaInList));
        }

        // for debugging
        private SyntaxTrivia[] Nodes => this.ToArray();

        IEnumerator<SyntaxTrivia> IEnumerable<SyntaxTrivia>.GetEnumerator()
        {
            if (Node == null)
            {
                return SpecializedCollections.EmptyEnumerator<SyntaxTrivia>();
            }

            return new EnumeratorImpl(this);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            if (Node == null)
            {
                return SpecializedCollections.EmptyEnumerator<SyntaxTrivia>();
            }

            return new EnumeratorImpl(this);
        }

        /// <summary>
        /// get the green node at the specific slot
        /// </summary>
        private SyntaxNode GetNodeAt(int i)
        {
            return GetNodeAt(Node, i);
        }

        private static SyntaxNode GetNodeAt(SyntaxNode node, int i)
        {
            Debug.Assert(node.IsList || (i == 0 && !node.IsList));
            return node.IsList ? node.GetNodeSlot(i) : node;
        }

        public bool Equals(SyntaxTriviaList other)
        {
            return Node == other.Node && Index == other.Index;
        }

        public static bool operator ==(SyntaxTriviaList left, SyntaxTriviaList right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(SyntaxTriviaList left, SyntaxTriviaList right)
        {
            return !left.Equals(right);
        }

        public override bool Equals(object obj)
        {
            return (obj is SyntaxTriviaList) && Equals((SyntaxTriviaList)obj);
        }

        public override int GetHashCode()
        {
            var hash = HashCodeCombiner.Start();
            hash.Add(Node);
            hash.Add(Index);
            return hash.CombinedHash;
        }

        /// <summary>
        /// Copy <paramref name="count"/> number of items starting at <paramref name="offset"/> from this list into <paramref name="array"/> starting at <paramref name="arrayOffset"/>.
        /// </summary>
        internal void CopyTo(int offset, SyntaxTrivia[] array, int arrayOffset, int count)
        {
            if (offset < 0 || count < 0 || Count < offset + count)
            {
                throw new IndexOutOfRangeException();
            }

            if (count == 0)
            {
                return;
            }

            // get first one without creating any red node
            var first = this[offset];
            array[arrayOffset] = first;

            // calculate trivia position from the first ourselves from now on
            var position = first.Position;
            var current = first;

            for (var i = 1; i < count; i++)
            {
                position += current.FullWidth;
                current = GetNodeAt(offset + i) as SyntaxTrivia;

                array[arrayOffset + i] = current;
            }
        }

        public override string ToString()
        {
            return Node != null ? Node.ToString() : string.Empty;
        }

        public string ToFullString()
        {
            return Node != null ? Node.ToFullString() : string.Empty;
        }

        public static SyntaxTriviaList Create(SyntaxTrivia trivia)
        {
            return new SyntaxTriviaList(trivia);
        }

        /// <summary>
        /// Reversed enumerable.
        /// </summary>
        public struct Reversed : IEnumerable<SyntaxTrivia>, IEquatable<Reversed>
        {
            private readonly SyntaxTriviaList _list;

            public Reversed(in SyntaxTriviaList list)
            {
                _list = list;
            }

            public Enumerator GetEnumerator()
            {
                return new Enumerator(in _list);
            }

            IEnumerator<SyntaxTrivia> IEnumerable<SyntaxTrivia>.GetEnumerator()
            {
                if (_list.Count == 0)
                {
                    return SpecializedCollections.EmptyEnumerator<SyntaxTrivia>();
                }

                return new ReversedEnumeratorImpl(in _list);
            }

            IEnumerator
                IEnumerable.GetEnumerator()
            {
                if (_list.Count == 0)
                {
                    return SpecializedCollections.EmptyEnumerator<SyntaxTrivia>();
                }

                return new ReversedEnumeratorImpl(in _list);
            }

            public override int GetHashCode()
            {
                return _list.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                return obj is Reversed && Equals((Reversed)obj);
            }

            public bool Equals(Reversed other)
            {
                return _list.Equals(other._list);
            }

            [StructLayout(LayoutKind.Auto)]
            public struct Enumerator
            {
                private readonly SyntaxNode _singleNodeOrList;
                private readonly int _baseIndex;
                private readonly int _count;

                private int _index;
                private SyntaxNode _current;
                private int _position;

                public Enumerator(in SyntaxTriviaList list)
                    : this()
                {
                    if (list.Any())
                    {
                        _singleNodeOrList = list.Node;
                        _baseIndex = list.Index;
                        _count = list.Count;

                        _index = _count;
                        _current = null;

                        var last = list.Last();
                        _position = last.Position + last.FullWidth;
                    }
                }

                public bool MoveNext()
                {
                    if (_count == 0 || _index <= 0)
                    {
                        _current = null;
                        return false;
                    }

                    _index--;

                    _current = GetNodeAt(_singleNodeOrList, _index);
                    _position -= _current.FullWidth;

                    return true;
                }

                public SyntaxTrivia Current
                {
                    get
                    {
                        if (_current == null)
                        {
                            throw new InvalidOperationException();
                        }

                        return (SyntaxTrivia)_current;
                    }
                }
            }

            private class ReversedEnumeratorImpl : IEnumerator<SyntaxTrivia>
            {
                private Enumerator _enumerator;

                // SyntaxTriviaList is a relatively big struct so is passed as ref
                internal ReversedEnumeratorImpl(in SyntaxTriviaList list)
                {
                    _enumerator = new Enumerator(in list);
                }

                public SyntaxTrivia Current => _enumerator.Current;

                object IEnumerator.Current => _enumerator.Current;

                public bool MoveNext()
                {
                    return _enumerator.MoveNext();
                }

                public void Reset()
                {
                    throw new NotSupportedException();
                }

                public void Dispose()
                {
                }
            }
        }

        [StructLayout(LayoutKind.Auto)]
        public struct Enumerator
        {
            private SyntaxNode _singleNodeOrList;
            private int _baseIndex;
            private int _count;

            private int _index;
            private SyntaxNode _current;
            private int _position;

            internal Enumerator(in SyntaxTriviaList list)
            {
                _singleNodeOrList = list.Node;
                _baseIndex = list.Index;
                _count = list.Count;

                _index = -1;
                _current = null;
                _position = list.Position;
            }

            private void InitializeFrom(SyntaxNode node, int index, int position)
            {
                _singleNodeOrList = node;
                _baseIndex = index;
                _count = node.IsList ? node.SlotCount : 1;

                _index = -1;
                _current = null;
                _position = position;
            }

            // PERF: Used to initialize an enumerator for leading trivia directly from a token.
            // This saves constructing an intermediate SyntaxTriviaList. Also, passing token
            // by ref since it's a non-trivial struct
            internal void InitializeFromLeadingTrivia(in SyntaxToken token)
            {
                InitializeFrom(token.GetLeadingTrivia().Node, 0, token.Position);
            }

            // PERF: Used to initialize an enumerator for trailing trivia directly from a token.
            // This saves constructing an intermediate SyntaxTriviaList. Also, passing token
            // by ref since it's a non-trivial struct
            internal void InitializeFromTrailingTrivia(in SyntaxToken token)
            {
                var leading = token.GetLeadingTrivia().Node;
                var index = 0;
                if (leading != null)
                {
                    index = leading.IsList ? leading.SlotCount : 1;
                }

                var trailing = token.GetTrailingTrivia().Node;
                var trailingPosition = token.Position + token.FullWidth;
                if (trailing != null)
                {
                    trailingPosition -= trailing.FullWidth;
                }

                InitializeFrom(trailing, index, trailingPosition);
            }

            public bool MoveNext()
            {
                var newIndex = _index + 1;
                if (newIndex >= _count)
                {
                    // invalidate iterator
                    _current = null;
                    return false;
                }

                _index = newIndex;

                if (_current != null)
                {
                    _position += _current.FullWidth;
                }

                _current = GetNodeAt(_singleNodeOrList, newIndex);
                return true;
            }

            public SyntaxTrivia Current
            {
                get
                {
                    if (_current == null)
                    {
                        throw new InvalidOperationException();
                    }

                    return _current as SyntaxTrivia;
                }
            }

            internal bool TryMoveNextAndGetCurrent(out SyntaxTrivia current)
            {
                if (!MoveNext())
                {
                    current = default;
                    return false;
                }

                current = _current as SyntaxTrivia;
                return true;
            }
        }

        private class EnumeratorImpl : IEnumerator<SyntaxTrivia>
        {
            private Enumerator _enumerator;

            // SyntaxTriviaList is a relatively big struct so is passed as ref
            internal EnumeratorImpl(in SyntaxTriviaList list)
            {
                _enumerator = new Enumerator(list);
            }

            public SyntaxTrivia Current => _enumerator.Current;

            object IEnumerator.Current => _enumerator.Current;

            public bool MoveNext()
            {
                return _enumerator.MoveNext();
            }

            public void Reset()
            {
                throw new NotSupportedException();
            }

            public void Dispose()
            {
            }
        }
    }
}
