using System.Linq;

namespace System.Collections.Generic;

/// <summary>
/// Цикличный массив
/// </summary>
/// <typeparam name="T"></typeparam>
public class CyclicArray<T> : IEnumerable<T>
{
    readonly T[] array;
    int used, current = -1;

    /// <summary>
    /// constructor
    /// </summary>
    /// <param name="capacity"></param>
    public CyclicArray(int capacity)
    {
        array = new T[capacity];
    }

    /// <summary>
    /// Количество элементов в массиве
    /// </summary>
    public int Count => used;

    /// <summary>
    /// Добавить новый элемент<br/>
    /// Если capacity достигнуто, то вытеснит самый первый элемент
    /// </summary>
    /// <param name="item"></param>
    public void Add(T item)
    {
        current = (++current) % array.Length;
        array[current] = item;
        if (used < array.Length)
            used++;
        UpdatedInvoke(current, item);
    }

    /// <summary>
    /// Добавить новые элементы.<br/>
    /// Если capacity достигнуто, то вытеснит самые первые элементы.<br/>
    /// Не вызывает Updated!
    /// </summary>
    /// <param name="items"></param>
    public void AddRange(IEnumerable<T> items)
    {
        var capacity = array.Length;
        var newItems = items.TakeLast(capacity).ToArray();
        if (newItems.Length == capacity)
        {
            Array.Copy(newItems, array, capacity);
            current = capacity - 1;
        }
        else
        {
            current = (++current) % capacity;
            var countToEndOfArray = capacity - current;
            if (newItems.Length <= countToEndOfArray)
            {
                Array.Copy(newItems, 0, array, current, newItems.Length);
                current += newItems.Length - 1;
            }
            else
            {
                var firstStepLength = countToEndOfArray;
                Array.Copy(newItems, 0, array, current, firstStepLength);
                var secondStepCount = newItems.Length - firstStepLength;
                Array.Copy(newItems, firstStepLength, array, 0, secondStepCount);
                current = secondStepCount - 1;
            }
        }

        if (used < capacity)
        {
            used += newItems.Length;
            used = used > capacity ? capacity : used;
        }
    }

    /// <summary>
    /// Индекс
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public T this[int index]
    {
        get
        {
            if (used == 0)
                throw new IndexOutOfRangeException();

            var i = (current + 1 + index) % used;
            return array[i];
        }
        set
        {
            var devider = used > 0 ? used : array.Length;
            var i = (current + 1 + index) % devider;
            array[i] = value;
            UpdatedInvoke(current, value);
        }
    }

    /// <summary>
    /// событие на изменение элемента в массиве
    /// </summary>
    public event EventHandler<(int index, T value)>? Updated;
    private void UpdatedInvoke(int index, T value)
    {
        Updated?.Invoke(this, (index, value));
    }

    /// <summary>
    /// Агрегирование значения по всему массиву
    /// </summary>
    /// <typeparam name="Tout"></typeparam>
    /// <param name="func"></param>
    /// <param name="startValue"></param>
    /// <returns></returns>
    public Tout Aggregate<Tout>(Func<T, Tout, Tout> func, Tout startValue)
    {
        Tout result = startValue;
        for (int i = 0; i < used; i++)
            result = func(this[i], result);
        return result;
    }

    /// <inheritdoc/>
    public IEnumerator<T> GetEnumerator()
        => new CyclycListEnumerator<T>(array, current, used);

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

    class CyclycListEnumerator<Te> : IEnumerator<Te>
    {
        private readonly Te[] array;
        private readonly int used;
        private readonly int first;
        private int current = -1;

        public CyclycListEnumerator(Te[] array, int first, int used)
        {
            this.array = new Te[array.Length];
            array.CopyTo(this.array, 0);
            this.used = used;
            this.first = first;
        }

        public Te Current
        {
            get
            {
                if (IsCurrentOk())
                {
                    var i = (current + first + 1) % used;
                    return array[i];
                }
                else
                    return default!;
            }
        }

        object? IEnumerator.Current => Current;

        public void Dispose() {; }

        private bool IsCurrentOk() => current >= 0 && current < used;

        public bool MoveNext()
        {
            if (current < used)
                current++;
            return IsCurrentOk();
        }

        public void Reset()
        {
            current = -1;
        }
    }

    /// <summary>
    /// Очистить весь массив
    /// </summary>
    public void Clear()
    {
        used = 0;
        current = -1;
    }
}