namespace Zinnia.Data.Collection.List
{
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Zinnia.Data.Type;
using Zinnia.Extension;
///
/// The basis for all Observable List types.
///
public abstract class ObservableList : MonoBehaviour { }
///
/// Allows observing changes to a .
///
/// The type of the elements in the .
/// The type to use.
public abstract class ObservableList : ObservableList where TEvent : UnityEvent, new()
{
#region List Contents Events
///
/// Emitted when the element at a given index is obtained.
///
[Header("List Contents Events")]
public TEvent Obtained = new TEvent();
///
/// Emitted when the searched element is found.
///
public TEvent Found = new TEvent();
///
/// Emitted when the searched element is not found.
///
public TEvent NotFound = new TEvent();
///
/// Emitted when the collection contents is checked but is empty.
///
public UnityEvent IsEmpty = new UnityEvent();
///
/// Emitted when the collection contents is checked and is populated.
///
public UnityEvent IsPopulated = new UnityEvent();
#endregion
#region List Mutation Events
///
/// Emitted when the first element is added to the collection.
///
[Header("List Mutation Events")]
public TEvent Populated = new TEvent();
///
/// Emitted when an element is added to the collection.
///
public TEvent Added = new TEvent();
///
/// Emitted when an element is removed from the collection.
///
public TEvent Removed = new TEvent();
///
/// Emitted when the last element is removed from the collection.
///
public TEvent Emptied = new TEvent();
#endregion
[Header("List Settings")]
[Tooltip("The index to use in methods specifically specifying to use it. In case this index is out of bounds for the collection it will be clamped within the index bounds.")]
[SerializeField]
private int currentIndex;
///
/// The index to use in methods specifically specifying to use it. In case this index is out of bounds for the collection it will be clamped within the index bounds.
///
public int CurrentIndex
{
get
{
return currentIndex;
}
set
{
currentIndex = value;
}
}
///
/// The elements to observe changes of, accessible from components that *are* keeping in sync with the state of the collection by subscribing to the list mutation events. Alternatively use instead.
///
public virtual HeapAllocationFreeReadOnlyList SubscribableElements => wasStartCalled ? (HeapAllocationFreeReadOnlyList)Elements : Array.Empty();
///
/// The elements to observe changes of, accessible from components that are *not* interested in keeping in sync with the state of the collection. Alternatively use instead.
///
public virtual HeapAllocationFreeReadOnlyList NonSubscribableElements => Elements;
///
/// The collection to observe changes of.
///
protected abstract List Elements { get; set; }
///
/// Whether was called.
///
protected bool wasStartCalled;
///
/// Gets the element at the given index.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The index in the collection to retrieve from. In case this index is out of bounds for the collection it will be clamped within the index bounds.
/// The element at the given index.
public virtual TElement Get(int index)
{
index = Elements.ClampIndex(index);
if (this.IsValidState())
{
Obtained?.Invoke(Elements[index]);
}
return Elements[index];
}
///
/// Gets the element at the given index.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The index in the collection to retrieve from. In case this index is out of bounds for the collection it will be clamped within the index bounds.
public virtual void DoGet(int index)
{
Get(index);
}
///
/// Checks to see if the collection contains the given element.
///
/// The element to search for.
/// Whether the element is found.
public virtual bool Contains(TElement element)
{
if (Elements.Contains(element))
{
if (this.IsValidState())
{
Found?.Invoke(element);
}
return true;
}
if (this.IsValidState())
{
NotFound?.Invoke(element);
}
return false;
}
///
/// Checks to see if the collection contains the given element.
///
/// The element to search for.
public virtual void DoContains(TElement element)
{
Contains(element);
}
///
/// Checks to see if the collection is currently empty.
///
/// Whether the collection is empty.
public virtual bool IsListEmpty()
{
if (Elements.Count == 0)
{
if (this.IsValidState())
{
IsEmpty?.Invoke();
}
return true;
}
else
{
if (this.IsValidState())
{
IsPopulated?.Invoke();
}
return false;
}
}
///
/// Checks to see if the collection is currently empty.
///
public virtual void DoIsListEmpty()
{
IsListEmpty();
}
///
/// Gets the index of the given element.
///
/// The element to check for.
/// The index of the found element.
public virtual int IndexOf(TElement element)
{
return Elements.IndexOf(element);
}
///
/// Adds an element to the end of the collection.
///
/// The element to add.
public virtual void Add(TElement element)
{
if (!this.IsValidState())
{
return;
}
Elements.Add(element);
EmitAddEvents(element);
}
///
/// Adds an element to the end of the collection as long as it does not already exist in the collection.
///
/// The unique element to add.
public virtual void AddUnique(TElement element)
{
if (!this.IsValidState() || Elements.Contains(element))
{
return;
}
Add(element);
}
///
/// Inserts an element to the given index of the collection.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The element to insert.
/// The index to insert at. If the index is below the lower bounds it will be clamped at the lower bound of the index, if the index is above the upper bounds then a new element will be added to the end of the collection.
public virtual void InsertAt(TElement element, int index)
{
if (!this.IsValidState())
{
return;
}
if (Elements.Count == 0 || index >= Elements.Count)
{
Add(element);
return;
}
index = Elements.ClampIndex(index);
Elements.Insert(index, element);
EmitAddEvents(element);
}
///
/// Inserts an element to the given index of the collection as long as it does not already exist in the collection.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The unique element to insert.
/// The index to insert at. If the index is below the lower bounds it will be clamped at the lower bound of the index, if the index is above the upper bounds then a new element will be added to the end of the collection.
public virtual void InsertUniqueAt(TElement element, int index)
{
if (!this.IsValidState() || Elements.Contains(element))
{
return;
}
InsertAt(element, index);
}
///
/// Inserts an element at the .
///
/// The element to insert.
public virtual void InsertAtCurrentIndex(TElement element)
{
if (!this.IsValidState())
{
return;
}
InsertAt(element, CurrentIndex);
}
///
/// Adds an element at the as long as it does not already exist in the collection.
///
/// The unique element to add.
public virtual void AddUniqueAtCurrentIndex(TElement element)
{
if (!this.IsValidState() || Elements.Contains(element))
{
return;
}
InsertAtCurrentIndex(element);
}
///
/// Sets the given element at the given index.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The element to set.
/// The index in the collection to set at. In case this index is out of bounds for the collection it will be clamped within the index bounds.
public virtual void SetAt(TElement element, int index)
{
if (!this.IsValidState() || Elements.Count == 0)
{
return;
}
index = Elements.ClampIndex(index);
RemoveAt(index);
InsertAt(element, index);
}
///
/// Sets the given element at the given index as long as it does not already exist in the collection.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The unique element to set.
/// The index in the collection to set at. In case this index is out of bounds for the collection it will be clamped within the index bounds.
public virtual void SetUniqueAt(TElement element, int index)
{
if (!this.IsValidState() || Elements.Contains(element))
{
return;
}
SetAt(element, index);
}
///
/// Sets the given element at the .
///
/// The element to set.
public virtual void SetAtCurrentIndex(TElement element)
{
if (!this.IsValidState())
{
return;
}
SetAt(element, CurrentIndex);
}
///
/// Sets the given element at the as long as it does not already exist in the collection.
///
/// The unique element to set.
public virtual void SetUniqueAtCurrentIndex(TElement element)
{
if (!this.IsValidState())
{
return;
}
SetUniqueAt(element, CurrentIndex);
}
///
/// Sets the given element at the given index or adds the element if the collection is empty.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The element to set.
/// The index in the collection to set at. In case this index is out of bounds for the collection it will be clamped within the index bounds.
public virtual void SetAtOrAddIfEmpty(TElement element, int index)
{
if (!this.IsValidState())
{
return;
}
if (Elements.Count == 0)
{
Add(element);
return;
}
SetAt(element, index);
}
///
/// Sets the given element at the given index as long as it does not already exist in the collection or adds the element if the collection is empty.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The unique element to set.
/// The index in the collection to set at. In case this index is out of bounds for the collection it will be clamped within the index bounds.
public virtual void SetUniqueAtOrAddIfEmpty(TElement element, int index)
{
if (!this.IsValidState() || Elements.Contains(element))
{
return;
}
SetAtOrAddIfEmpty(element, index);
}
///
/// Sets the given element at the or adds the element if the collection is empty.
///
/// The element to set.
public virtual void SetAtCurrentIndexOrAddIfEmpty(TElement element)
{
if (!this.IsValidState())
{
return;
}
SetAtOrAddIfEmpty(element, CurrentIndex);
}
///
/// Sets the given element at the as long as it does not already exist in the collection or adds the element if the collection is empty.
///
/// The unique element to set.
public virtual void SetUniqueAtCurrentIndexOrAddIfEmpty(TElement element)
{
if (!this.IsValidState())
{
return;
}
SetUniqueAtOrAddIfEmpty(element, CurrentIndex);
}
///
/// Removes the first occurrence of an element from the collection.
///
/// The element to remove.
public virtual bool Remove(TElement element)
{
if (!this.IsValidState())
{
return default;
}
if (Elements.Remove(element))
{
EmitRemoveEvents(element);
return true;
}
return false;
}
///
/// Removes the first occurrence of an element from the collection.
///
public virtual void DoRemove(TElement element)
{
Remove(element);
}
///
/// Removes the last occurrence of an element from the collection.
///
/// The element to remove.
public virtual void RemoveLastOccurrence(TElement element)
{
if (!this.IsValidState())
{
return;
}
int index = Elements.LastIndexOf(element);
if (index == -1)
{
return;
}
RemoveAt(index);
}
///
/// Removes an element at the given index from the collection.
///
///
/// Allows the use of a clamped index to prevent indices being out of bounds and doing negative queries such as `-1` sets the last element.
///
/// The index to remove at. In case this index is out of bounds for the collection it will be clamped within the index bounds.
public virtual void RemoveAt(int index)
{
if (!this.IsValidState() || Elements.Count == 0)
{
return;
}
index = Elements.ClampIndex(index);
TElement removedElement = Elements[index];
Elements.RemoveAt(index);
EmitRemoveEvents(removedElement);
}
///
/// Removes an element at the from the collection.
///
public virtual void RemoveAtCurrentIndex()
{
if (!this.IsValidState())
{
return;
}
RemoveAt(CurrentIndex);
}
///
/// Removes all elements from the collection.
///
/// Whether to reverse the collection when clearing so the removal goes from end to start.
public virtual void Clear(bool removeFromEndToStart = false)
{
if (!this.IsValidState() || Elements.Count == 0)
{
return;
}
if (removeFromEndToStart)
{
Elements.Reverse();
}
foreach (TElement element in Elements)
{
Removed?.Invoke(element);
}
Elements.Clear();
Emptied?.Invoke(default);
}
protected virtual void Start()
{
wasStartCalled = true;
if (Elements == null)
{
return;
}
for (int index = 0; index < Elements.Count; index++)
{
TElement element = Elements[index];
if (EqualityComparer.Default.Equals(element, default))
{
continue;
}
Added?.Invoke(element);
if (index == 0)
{
Populated?.Invoke(element);
}
}
}
///
/// Always emits and additionally if the first element was added to the collection.
///
/// The element that was added.
protected virtual void EmitAddEvents(TElement element)
{
if (!wasStartCalled)
{
return;
}
Added?.Invoke(element);
if (Elements.Count == 1)
{
Populated?.Invoke(element);
}
}
///
/// Always emits and additionally if the last element was removed from the collection.
///
/// The element that was removed.
protected virtual void EmitRemoveEvents(TElement element)
{
Removed?.Invoke(element);
if (Elements.Count == 0)
{
Emptied?.Invoke(element);
}
}
}
}