// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; namespace FlaxEngine.Collections { /// /// Creates new structure array like, with fast front and back insertion. /// Every overflow of this buffer removes last item form other side of insertion /// /// This collection is NOT thread-safe. /// Type of items inserted into buffer [Serializable] [JsonObject(MemberSerialization.OptIn)] public class CircularBuffer : IEnumerable { /// /// Arguments for new item added event /// public class ItemAddedEventArgs : EventArgs { /// /// Gets Index of new element in buffer /// public int Index { get; } /// /// Gets added item /// public T Item { get; } /// /// Initializes a new instance of the class. /// /// The index. /// The item. public ItemAddedEventArgs(int index, T item) { Index = index; Item = item; } } /// /// Arguments for item removed event /// public class ItemRemovedEventArgs : EventArgs { /// /// Gets if item removed was item from front of the buffer /// public bool WasFrontItem { get; } /// /// Gets removed item /// public T Item { get; } /// /// Initializes a new instance of the class. /// /// if set to true [was front item]. /// The item. public ItemRemovedEventArgs(bool wasFrontItem, T item) { WasFrontItem = wasFrontItem; Item = item; } } /// /// Arguments for item being replaced because of buffer was overflown with data /// public class ItemOverflownEventArgs : EventArgs { /// /// Gets if item removed was item from front of the buffer /// public bool WasFrontItem { get; } /// /// Gets overflown item /// public T Item { get; } /// /// Initializes a new instance of the class. /// /// if set to true [was front item]. /// The item. public ItemOverflownEventArgs(bool wasFrontItem, T item) { WasFrontItem = wasFrontItem; Item = item; } } [JsonProperty("buffer"), Serialize] private T[] _buffer; [JsonProperty("frontItem"), Serialize] private int _frontItem; [JsonProperty("backItem"), Serialize] private int _backItem; /// /// Executes an action when item is removed /// public event ItemRemovedEventHandler OnItemRemoved; /// public delegate void ItemRemovedEventHandler(object sender, ItemRemovedEventArgs e); /// /// Executes an action when item is added /// public event ItemAddedEventHandler OnItemAdded; /// public delegate void ItemAddedEventHandler(object sender, ItemAddedEventArgs e); /// /// Executes an action when item is removed because of overflow in buffer /// public event ItemOverflownEventHandler OnItemOverflown; /// public delegate void ItemOverflownEventHandler(object sender, ItemOverflownEventArgs e); /// /// Amount of items currently in buffer /// public int Count { get; private set; } /// /// Current capacity of internal buffer /// public int Capacity { get => _buffer.Length; set { if (value <= 0) throw new ArgumentOutOfRangeException(); if (value == _buffer.Length) return; if (Count > 0) throw new InvalidOperationException("Cannot change capacity for non-empty buffer."); _buffer = new T[value]; } } /// /// Returns true if there are no items in structure, or false if there are /// public bool IsEmpty => Count == 0; /// /// Returns true if buffer is filled with whole of its capacity with items /// public bool IsFull => Count == Capacity; /// /// Creates new instance of object with given capacity, copies given array as a framework /// /// Buffer to insert into /// First index of an item in provided buffer /// Last index on an item in provided buffer [JsonConstructor] public CircularBuffer(IEnumerable buffer, int frontItem = 0, int backItem = 0) { var insertionArray = buffer.ToArray(); if (insertionArray.Length < frontItem) throw new ArgumentOutOfRangeException(nameof(frontItem), "argument cannot be larger then requested capacity"); if (-1 > frontItem) throw new ArgumentOutOfRangeException(nameof(frontItem), "argument cannot be smaller then -1"); if (insertionArray.Length < backItem) throw new ArgumentOutOfRangeException(nameof(frontItem), "argument cannot be larger then requested capacity"); if (-1 > backItem) throw new ArgumentOutOfRangeException(nameof(frontItem), "argument cannot be smaller then -1"); _buffer = insertionArray; _backItem = backItem; _frontItem = frontItem; Count = insertionArray.Length; } /// /// Creates new instance of object with given capacity /// /// Capacity of internal structure public CircularBuffer(int capacity) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), "argument cannot be lower or equal zero"); _buffer = new T[capacity]; _backItem = 0; _frontItem = 0; Count = 0; } /// /// Creates new instance of object with given capacity and adds array of items to internal buffer /// /// Capacity of internal structure /// Items to input /// Index of items to input at in internal buffer public CircularBuffer(int capacity, T[] items, int arrayIndex = 0) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), "argument cannot be lower or equal zero"); if (items.Length + arrayIndex > capacity) throw new ArgumentOutOfRangeException(nameof(items), "argument cannot be larger then requested capacity with moved arrayIndex"); _buffer = new T[capacity]; items.CopyTo(_buffer, arrayIndex); _backItem = arrayIndex; _frontItem = items.Length + arrayIndex; if (items.Length > 0) _frontItem -= 1; Count = items.Length; } /// /// Gets or sets item from list at given index. /// All items are in order of input regardless of overflow that may occur /// /// Index to item required public T this[int index] { get { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), "argument cannot be lower then zero"); if (index >= Count) throw new IndexOutOfRangeException("argument cannot be bigger then amount of elements"); var currentIndex = (index + _backItem) % Capacity; return _buffer[currentIndex]; } set { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), "argument cannot be lower then zero"); if (index >= Count) throw new IndexOutOfRangeException("argument cannot be bigger then amount of elements"); var currentIndex = (index + _backItem) % Capacity; _buffer[currentIndex] = value; } } /// /// Adds item to the front of the buffer /// /// Item to add public void PushFront(T item) { if (!IsEmpty) IncreaseFrontIndex(); OnItemAdded?.Invoke(this, new ItemAddedEventArgs(Count - 1, item)); if (Count == Capacity) OnItemOverflown?.Invoke(this, new ItemOverflownEventArgs(false, _buffer[_frontItem])); _buffer[_frontItem] = item; if (Count < Capacity) Count++; } /// /// Adds item to the back of the buffer /// /// Item to add public void PushBack(T item) { if (!IsEmpty) DecreaseBackIndex(); OnItemAdded?.Invoke(this, new ItemAddedEventArgs(0, item)); if (Count == Capacity) OnItemOverflown?.Invoke(this, new ItemOverflownEventArgs(true, _buffer[_backItem])); _buffer[_backItem] = item; if (Count < Capacity) Count++; } /// /// Gets top first element form collection /// public T Front() { if (Count == 0) throw new IndexOutOfRangeException("Collection cannot be empty"); return _buffer[_frontItem]; } /// /// Gets bottom first element form collection /// public T Back() { if (Count == 0) throw new IndexOutOfRangeException("Collection cannot be empty"); return _buffer[_backItem]; } /// /// Removes first item from the front of the buffer /// /// public T PopFront() { if (IsEmpty) throw new IndexOutOfRangeException("You cannot remove item from empty collection"); var result = Front(); Count--; if (!IsEmpty) { DecreaseFrontIndex(); } else { _frontItem = 0; _backItem = 0; } OnItemRemoved?.Invoke(this, new ItemRemovedEventArgs(true, result)); return result; } /// /// Removes first item from the back of the buffer /// /// public T PopBack() { if (IsEmpty) throw new IndexOutOfRangeException("You cannot remove item from empty collection"); var result = Back(); Count--; if (!IsEmpty) { IncreaseBackIndex(); } else { _frontItem = 0; _backItem = 0; } OnItemRemoved?.Invoke(this, new ItemRemovedEventArgs(false, result)); return result; } /// /// Copies the buffer contents to an array, according to the logical /// contents of the buffer (i.e. independent of the internal /// order/contents) /// /// A new array with a copy of the buffer contents. public T[] ToArray() { if (Count == 0) return Utils.GetEmptyArray(); var result = new T[Count]; if (_backItem > _frontItem) { Array.Copy(_buffer, _backItem, result, 0, Capacity - _backItem); Array.Copy(_buffer, 0, result, Capacity - _backItem, _frontItem + 1); } else { Array.Copy(_buffer, _backItem, result, 0, _frontItem - _backItem + 1); } return result; } /// /// CopyTo copies a collection into an Array, starting at a particular index into the array. /// /// A new array with a copy of the buffer contents. public void CopyTo(T[] array, int arrayIndex) { ToArray().CopyTo(array, arrayIndex); } /// /// Clears buffer and remains capacity /// public void Clear() { if (Count > 0) { _buffer = new T[Capacity]; _frontItem = 0; _backItem = 0; Count = 0; } } /// /// Clears buffer and changes its capacity. /// /// The new capacity of the buffer. public void Clear(int newCapacity) { if (newCapacity <= 0) throw new ArgumentOutOfRangeException(); _buffer = new T[newCapacity]; _frontItem = 0; _backItem = 0; Count = 0; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// Returns an enumerator that iterates through the collection. /// /// /// A that can be used to iterate through the collection. /// public IEnumerator GetEnumerator() { if (IsEmpty) yield break; var array = ToArray(); for (int i = 0; i < array.Length; i++) yield return array[i]; } /// /// Decrease index of _backItem and warp it round if fall below 0 /// Move _frontItem back index if they've met /// private void DecreaseBackIndex() { var currentIndex = --_backItem % Capacity; if (currentIndex < 0) currentIndex = Capacity + currentIndex; _backItem = currentIndex; if (_backItem == _frontItem && IsFull) DecreaseFrontIndex(); } /// /// Decrease index of _frontItem and warp it round if fall below 0 /// Move _backItem back index if they've met /// private void DecreaseFrontIndex() { var currentIndex = --_frontItem % Capacity; if (currentIndex < 0) currentIndex = Capacity + currentIndex; _frontItem = currentIndex; if (_backItem == _frontItem && IsFull) DecreaseBackIndex(); } /// /// Increases index of _backItem and warp it round if exceded capacity /// Move _frontItem forward index if they've met /// private void IncreaseBackIndex() { _backItem = ++_backItem % Capacity; if (_backItem == _frontItem) IncreaseFrontIndex(); } /// /// Increases index of _frontItem and warp it round if exceded capacity /// Move _backItem forward index if they've met /// private void IncreaseFrontIndex() { _frontItem = ++_frontItem % Capacity; if (_backItem == _frontItem) IncreaseBackIndex(); } } }