// Copyright (c) 2012-2021 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();
}
}
}