// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.DataStructure
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using ProtoBuf;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
///
/// A highly optimized double-ended queue (deque) implemented with a circular array.
/// Supports efficient O(1) insertion and removal from both front and back.
/// Ideal for BFS algorithms, undo/redo systems, and sliding window problems.
///
///
/// patrolPoints = new Deque();
/// patrolPoints.PushBack(startPoint);
/// patrolPoints.PushBack(nextPoint);
/// Vector3 current = patrolPoints.PopFront();
/// patrolPoints.PushBack(current); // cycle patrol
/// ]]>
///
/// The type of elements in the deque.
[Serializable]
[ProtoContract(IgnoreListHandling = true)]
public sealed class Deque : IReadOnlyList
{
public const int DefaultCapacity = 16;
public struct DequeEnumerator : IEnumerator
{
private readonly T[] _items;
private readonly int _head;
private readonly int _count;
private readonly int _capacity;
private int _index;
private T _current;
internal DequeEnumerator(T[] items, int head, int count, int capacity)
{
_items = items;
_head = head;
_count = count;
_capacity = capacity;
_index = -1;
_current = default;
}
public bool MoveNext()
{
if (++_index < _count)
{
int actualIndex = (_head + _index) % _capacity;
_current = _items[actualIndex];
return true;
}
_current = default;
return false;
}
public T Current => _current;
object IEnumerator.Current => Current;
public void Reset()
{
_index = -1;
_current = default;
}
public void Dispose() { }
}
private const int MinimumGrowth = 4;
[SerializeField]
[ProtoIgnore]
private T[] _items;
[ProtoMember(1)]
private List _serializedItems;
[ProtoIgnore]
private PooledResource> _serializedItemsLease;
[SerializeField]
[ProtoMember(2)]
private int _head;
[SerializeField]
[ProtoMember(3)]
private int _tail;
[SerializeField]
[ProtoMember(4)]
private int _count;
[SerializeField]
[ProtoMember(5)]
private int _serializedCapacity;
///
/// Gets the number of elements in the deque.
///
public int Count => _count;
///
/// Gets whether the deque is empty.
///
public bool IsEmpty => _count == 0;
///
/// Gets the current capacity of the underlying array.
///
public int Capacity => _items.Length;
///
/// Gets the element at the specified index (0 is front, Count-1 is back).
///
public T this[int index]
{
get
{
if (index < 0 || index >= _count)
{
throw new IndexOutOfRangeException(
$"{index} is outside of bounds [0, {_count})"
);
}
int actualIndex = (_head + index) % _items.Length;
return _items[actualIndex];
}
set
{
if (index < 0 || index >= _count)
{
throw new IndexOutOfRangeException(
$"{index} is outside of bounds [0, {_count})"
);
}
int actualIndex = (_head + index) % _items.Length;
_items[actualIndex] = value;
}
}
private Deque()
{
_items = Array.Empty();
_head = 0;
_tail = 0;
_count = 0;
}
///
/// Constructs an empty deque with the specified capacity.
///
public Deque(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(capacity),
"Capacity must be positive."
);
}
_items = new T[capacity];
_head = 0;
_tail = 0;
_count = 0;
}
///
/// Constructs a deque from an existing collection.
///
public Deque(IEnumerable collection)
{
if (collection == null)
{
throw new ArgumentNullException(nameof(collection));
}
_head = 0;
_tail = 0;
_count = 0;
switch (collection)
{
case IReadOnlyList list:
{
int capacity = Math.Max(DefaultCapacity, list.Count);
_items = new T[capacity];
for (int i = 0; i < list.Count; i++)
{
PushBack(list[i]);
}
break;
}
case IReadOnlyCollection readOnlyCollection:
{
int capacity = Math.Max(DefaultCapacity, readOnlyCollection.Count);
_items = new T[capacity];
foreach (T item in readOnlyCollection)
{
PushBack(item);
}
break;
}
case ICollection inputCollection:
{
int capacity = Math.Max(DefaultCapacity, inputCollection.Count);
_items = new T[capacity];
foreach (T item in inputCollection)
{
PushBack(item);
}
break;
}
default:
{
_items = new T[DefaultCapacity];
foreach (T item in collection)
{
PushBack(item);
}
break;
}
}
}
///
/// Adds an element to the front of the deque in O(1) time.
///
public void PushFront(T item)
{
if (_count == _items.Length)
{
Resize(ComputeGrowth(_items.Length));
}
_head = (_head - 1 + _items.Length) % _items.Length;
_items[_head] = item;
_count++;
}
///
/// Adds an element to the back of the deque in O(1) time.
///
public void PushBack(T item)
{
if (_count == _items.Length)
{
Resize(ComputeGrowth(_items.Length));
}
_items[_tail] = item;
_tail = (_tail + 1) % _items.Length;
_count++;
}
///
/// Attempts to remove and return the element at the front of the deque.
///
/// True if an element was removed, false if empty.
public bool TryPopFront(out T result)
{
if (_count == 0)
{
result = default;
return false;
}
result = _items[_head];
_items[_head] = default;
_head = (_head + 1) % _items.Length;
_count--;
return true;
}
///
/// Attempts to remove and return the element at the back of the deque.
///
/// True if an element was removed, false if empty.
public bool TryPopBack(out T result)
{
if (_count == 0)
{
result = default;
return false;
}
_tail = (_tail - 1 + _items.Length) % _items.Length;
result = _items[_tail];
_items[_tail] = default;
_count--;
return true;
}
///
/// Attempts to peek at the front element without removing it.
///
/// True if the deque is not empty, false otherwise.
public bool TryPeekFront(out T result)
{
if (_count == 0)
{
result = default;
return false;
}
result = _items[_head];
return true;
}
///
/// Attempts to peek at the back element without removing it.
///
/// True if the deque is not empty, false otherwise.
public bool TryPeekBack(out T result)
{
if (_count == 0)
{
result = default;
return false;
}
int backIndex = (_tail - 1 + _items.Length) % _items.Length;
result = _items[backIndex];
return true;
}
///
/// Removes all elements from the deque.
///
public void Clear()
{
if (_count > 0)
{
if (_head < _tail)
{
Array.Clear(_items, _head, _count);
}
else
{
Array.Clear(_items, _head, _items.Length - _head);
Array.Clear(_items, 0, _tail);
}
}
_head = 0;
_tail = 0;
_count = 0;
}
///
/// Checks if the deque contains a specific element in O(n) time.
///
public bool Contains(T item)
{
EqualityComparer comparer = EqualityComparer.Default;
for (int i = 0; i < _count; i++)
{
int actualIndex = (_head + i) % _items.Length;
if (comparer.Equals(_items[actualIndex], item))
{
return true;
}
}
return false;
}
///
/// Copies the deque elements to an array.
///
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
if (arrayIndex < 0 || arrayIndex > array.Length)
{
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
}
if (array.Length - arrayIndex < _count)
{
throw new ArgumentException("Destination array is not large enough.");
}
for (int i = 0; i < _count; i++)
{
int actualIndex = (_head + i) % _items.Length;
array[arrayIndex + i] = _items[actualIndex];
}
}
///
/// Returns an array containing all elements in order.
///
public T[] ToArray()
{
T[] result = null;
_ = ToArray(ref result);
return result;
}
public int ToArray(ref T[] result)
{
if (result == null || result.Length < _count)
{
result = new T[_count];
}
CopyTo(result, 0);
return _count;
}
///
/// Trims excess capacity to match the current count.
///
public void TrimExcess()
{
int threshold = (int)(_items.Length * 0.9);
if (_count >= threshold)
{
return;
}
int newCapacity = Math.Max(DefaultCapacity, _count);
if (newCapacity < _items.Length)
{
Resize(newCapacity);
}
}
[ProtoBeforeSerialization]
private void OnProtoSerialize()
{
_serializedCapacity = _items.Length;
if (_count == 0)
{
_serializedItemsLease.Dispose();
_serializedItems = null;
return;
}
// Return any previous lease before renting a new one
_serializedItemsLease.Dispose();
// Rent a temporary list to avoid allocations during serialization
_serializedItemsLease = Buffers.List.Get(out List buffer);
for (int i = 0; i < _count; i++)
{
int actualIndex = (_head + i) % _items.Length;
buffer.Add(_items[actualIndex]);
}
_serializedItems = buffer;
}
[ProtoAfterSerialization]
private void OnProtoSerialized()
{
// Release rented list back to pool
_serializedItemsLease.Dispose();
_serializedItems = null;
}
[ProtoAfterDeserialization]
private void OnProtoDeserialized()
{
int itemCount = _serializedItems?.Count ?? 0;
int capacity = _serializedCapacity;
if (capacity <= 0)
{
capacity = itemCount > 0 ? itemCount : DefaultCapacity;
}
if (itemCount > capacity)
{
capacity = itemCount;
}
if (itemCount == 0)
{
_items = new T[capacity];
_head = 0;
_tail = 0;
_count = 0;
_serializedItems = null;
_serializedCapacity = _items.Length;
return;
}
_items = new T[capacity];
for (int i = 0; i < itemCount; i++)
{
_items[i] = _serializedItems[i];
}
_head = 0;
_count = itemCount;
_tail = itemCount < capacity ? itemCount : 0;
_serializedItems = null;
_serializedCapacity = _items.Length;
// Ensure no outstanding lease remains
_serializedItemsLease.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ComputeGrowth(int currentCapacity)
{
int growth = currentCapacity + (currentCapacity >> 1);
int newCapacity = currentCapacity + Math.Max(growth - currentCapacity, MinimumGrowth);
if ((uint)newCapacity > 0X7FFFFFC7)
{
newCapacity = 0X7FFFFFC7;
}
return newCapacity;
}
private void Resize(int newCapacity)
{
if (newCapacity <= _count)
{
return;
}
T[] newItems = new T[newCapacity];
if (_count > 0)
{
if (_head < _tail)
{
Array.Copy(_items, _head, newItems, 0, _count);
}
else
{
int headToEnd = _items.Length - _head;
Array.Copy(_items, _head, newItems, 0, headToEnd);
Array.Copy(_items, 0, newItems, headToEnd, _tail);
}
}
_items = newItems;
_head = 0;
_tail = _count;
}
public DequeEnumerator GetEnumerator()
{
return new DequeEnumerator(_items, _head, _count, _items.Length);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}