// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
namespace Animancer
{
/// Stores the index of an object in a .
/// https://kybernetik.com.au/animancer/api/Animancer/Key
///
public class Key : Key.IListItem
{
/************************************************************************************************************************/
/// An object with a so it can be used in a .
///
/// It's usually easiest to just inherit from , but otherwise the recommended
/// implementation looks like this:
///
/// class MyClass : Key.IListItem
/// {
/// Key Key.IListItem.Key { get; } = new Key();
/// // Don't use expression bodied ...Key => new... because that would create a new one every time.
/// }
///
/// https://kybernetik.com.au/animancer/api/Animancer/IListItem
///
public interface IListItem
{
///
/// The which stores the index of this object.
///
Key Key { get; }
}
/************************************************************************************************************************/
/// The which indicates that an item isn't in a list.
public const int NotInList = -1;
/// The current position of this key in the list.
private int _Index = -1;
/// Returns location of this object in the list (or -1 if it is not currently in a keyed list).
public static int IndexOf(Key key) => key._Index;
/// Is the `key` currently in a keyed list?
public static bool IsInList(Key key) => key._Index != NotInList;
/************************************************************************************************************************/
/// A is its own .
Key IListItem.Key => this;
/************************************************************************************************************************/
/// A which can remove items without needing to search the entire collection.
///
/// This implementation has several restrictions compared to a regular :
///
/// - Items must implement or inherit from .
/// - Items cannot be null.
/// - Items can only be in one at a time and cannot appear multiple times in it.
///
/// This class is nested inside so it can modify the private without
/// exposing that capability to anything else.
///
/// https://kybernetik.com.au/animancer/api/Animancer/KeyedList_1
///
public class KeyedList : IList, ICollection where T : class, IListItem
{
/************************************************************************************************************************/
private const string
SingleUse = "Each item can only be used in one " + nameof(KeyedList) + " at a time.",
NotFound = "The specified item does not exist in this " + nameof(KeyedList) + ".";
/************************************************************************************************************************/
private readonly List Items;
/************************************************************************************************************************/
/// Creates a new using the default constructor.
public KeyedList() => Items = new List();
/// Creates a new with the specified initial `capacity`.
public KeyedList(int capacity) => Items = new List(capacity);
// No copy constructor because the keys will not work if they are used in multiple lists at once.
/************************************************************************************************************************/
/// The number of items currently in the list.
public int Count => Items.Count;
/// The number of items that this list can contain before resizing is required.
public int Capacity
{
get => Items.Capacity;
set => Items.Capacity = value;
}
/************************************************************************************************************************/
/// The item at the specified `index`.
/// The `value` was already in a keyed list (setter only).
public T this[int index]
{
get => Items[index];
set
{
var key = value.Key;
// Make sure it isn't already in a list.
if (key._Index != NotInList)
throw new ArgumentException(SingleUse);
// Remove the old item at that index.
Items[index].Key._Index = NotInList;
// Set the index of the new item and add it at that index.
key._Index = index;
Items[index] = value;
}
}
/************************************************************************************************************************/
/// Indicates whether the `item` is currently in this list.
public bool Contains(T item)
{
if (item == null)
return false;
var index = item.Key._Index;
return
(uint)index < (uint)Items.Count &&
Items[index] == item;
}
/************************************************************************************************************************/
/// Returns the index of the `item` in this list or -1 if it is not in this list.
public int IndexOf(T item)
{
if (item == null)
return NotInList;
var index = item.Key._Index;
if ((uint)index < (uint)Items.Count &&
Items[index] == item)
return index;
else
return NotInList;
}
/************************************************************************************************************************/
/// Adds the `item` to the end of this list.
/// The `item` was already in a keyed list.
public void Add(T item)
{
var key = item.Key;
// Make sure it isn't already in a list.
if (key._Index != NotInList)
throw new ArgumentException(SingleUse);
// Set the index of the new item and add it to the list.
key._Index = Items.Count;
Items.Add(item);
}
/// Adds the `item` to the end of this list if it wasn't already in it.
public void AddNew(T item)
{
if (!Contains(item))
Add(item);
}
/************************************************************************************************************************/
/// Adds the `item` to this list at the specified `index`.
public void Insert(int index, T item)
{
for (int i = index; i < Items.Count; i++)
Items[i].Key._Index++;
item.Key._Index = index;
Items.Insert(index, item);
}
/************************************************************************************************************************/
/// Removes the item at the specified `index`.
public void RemoveAt(int index)
{
// Adjust the indices of all items after the target.
for (int i = index + 1; i < Items.Count; i++)
Items[i].Key._Index--;
// Mark the key as removed and remove the item.
Items[index].Key._Index = NotInList;
Items.RemoveAt(index);
}
/// Removes the item at the specified `index` by swapping the last item in this list into its place.
///
/// This does not maintain the order of items, but is more efficient than because
/// it avoids the need to move every item after the target down one place.
///
public void RemoveAtSwap(int index)
{
// Mark the item as removed.
Items[index].Key._Index = NotInList;
// If it wasn't the last item, move the last item over it.
var lastIndex = Items.Count - 1;
if (lastIndex > index)
{
var lastItem = Items[lastIndex];
lastItem.Key._Index = index;
Items[index] = lastItem;
}
// Remove the last item from the list.
Items.RemoveAt(lastIndex);
}
/************************************************************************************************************************/
/// Removes the `item` from this list.
/// The `item` is not in this list.
public bool Remove(T item)
{
var key = item.Key;
var index = key._Index;
// If it isn't in a list, do nothing.
if (index == NotInList)
return false;
// Make sure the item is actually in this list at the index it says.
// Otherwise it must be in a different list.
if (Items[index] != item)
throw new ArgumentException(NotFound, nameof(item));
// Remove the item.
RemoveAt(index);
return true;
}
/************************************************************************************************************************/
/// Removes the `item` by swapping the last item in this list into its place.
///
/// This does not maintain the order of items, but is more efficient than because
/// it avoids the need to move every item after the target down one place.
///
/// The `item` is not in this list.
public bool RemoveSwap(T item)
{
var key = item.Key;
var index = key._Index;
// If it isn't in a list, do nothing.
if (index == NotInList)
return false;
// Make sure the item is actually in this list at the index it says.
// Otherwise it must be in a different list.
if (Items[index] != item)
throw new ArgumentException(NotFound, nameof(item));
// Remove the item.
RemoveAtSwap(index);
return true;
}
/************************************************************************************************************************/
/// Removes all items from this list.
public void Clear()
{
for (int i = Items.Count - 1; i >= 0; i--)
Items[i].Key._Index = NotInList;
Items.Clear();
}
/************************************************************************************************************************/
/// Copies all the items from this list into the `array`, starting at the specified `index`.
public void CopyTo(T[] array, int index) => Items.CopyTo(array, index);
/// Copies all the items from this list into the `array`, starting at the specified `index`.
void ICollection.CopyTo(Array array, int index) => ((ICollection)Items).CopyTo(array, index);
/// Returns false.
bool ICollection.IsReadOnly => false;
/// Returns an enumerator that iterates through this list.
public List.Enumerator GetEnumerator() => Items.GetEnumerator();
///
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/************************************************************************************************************************/
/// Is this list thread safe?
bool ICollection.IsSynchronized => ((ICollection)Items).IsSynchronized;
/// An object that can be used to synchronize access to this .
object ICollection.SyncRoot => ((ICollection)Items).SyncRoot;
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}