// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Collections;
using System.Collections.Generic;

namespace osu.Game.Rulesets.Difficulty.Utils
{
    /// <summary>
    /// An indexed queue where items are indexed beginning from the most recently enqueued item.
    /// Enqueuing an item pushes all existing indexes up by one and inserts the item at index 0.
    /// Dequeuing an item removes the item from the highest index and returns it.
    /// </summary>
    public class ReverseQueue<T> : IEnumerable<T>
    {
        /// <summary>
        /// The number of elements in the <see cref="ReverseQueue{T}"/>.
        /// </summary>
        public int Count { get; private set; }

        private T[] items;
        private int capacity;
        private int start;

        public ReverseQueue(int initialCapacity)
        {
            if (initialCapacity <= 0)
                throw new ArgumentOutOfRangeException(nameof(initialCapacity));

            items = new T[initialCapacity];
            capacity = initialCapacity;
            start = 0;
            Count = 0;
        }

        /// <summary>
        /// Retrieves the item at an index in the <see cref="ReverseQueue{T}"/>.
        /// </summary>
        /// <param name="index">The index of the item to retrieve. The most recently enqueued item is at index 0.</param>
        public T this[int index]
        {
            get
            {
                if (index < 0 || index > Count - 1)
                    throw new ArgumentOutOfRangeException(nameof(index));

                int reverseIndex = Count - 1 - index;
                return items[(start + reverseIndex) % capacity];
            }
        }

        /// <summary>
        /// Enqueues an item to this <see cref="ReverseQueue{T}"/>.
        /// </summary>
        /// <param name="item">The item to enqueue.</param>
        public void Enqueue(T item)
        {
            if (Count == capacity)
            {
                // Double the buffer size
                var buffer = new T[capacity * 2];

                // Copy items to new queue
                for (int i = 0; i < Count; i++)
                {
                    buffer[i] = items[(start + i) % capacity];
                }

                // Replace array with new buffer
                items = buffer;
                capacity *= 2;
                start = 0;
            }

            items[(start + Count) % capacity] = item;
            Count++;
        }

        /// <summary>
        /// Dequeues the least recently enqueued item from the <see cref="ReverseQueue{T}"/> and returns it.
        /// </summary>
        /// <returns>The item dequeued from the <see cref="ReverseQueue{T}"/>.</returns>
        public T Dequeue()
        {
            var item = items[start];
            start = (start + 1) % capacity;
            Count--;
            return item;
        }

        /// <summary>
        /// Clears the <see cref="ReverseQueue{T}"/> of all items.
        /// </summary>
        public void Clear()
        {
            start = 0;
            Count = 0;
        }

        /// <summary>
        /// Returns an enumerator which enumerates items in the <see cref="ReverseQueue{T}"/> starting from the most recently enqueued item.
        /// </summary>
        public IEnumerator<T> GetEnumerator() => new Enumerator(this);

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

        public struct Enumerator : IEnumerator<T>
        {
            private ReverseQueue<T> reverseQueue;
            private int currentIndex;

            internal Enumerator(ReverseQueue<T> reverseQueue)
            {
                this.reverseQueue = reverseQueue;
                currentIndex = -1; // The first MoveNext() should bring the iterator to 0
            }

            public bool MoveNext() => ++currentIndex < reverseQueue.Count;

            public void Reset() => currentIndex = -1;

            public readonly T Current => reverseQueue[currentIndex];

            readonly object IEnumerator.Current => Current;

            public void Dispose()
            {
                reverseQueue = null;
            }
        }
    }
}
