Editor/HumanoidRetargeter/Embedded/Datamodel/Datamodel.ElementList.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeterDmx.Codecs;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace HumanoidRetargeterDmx
{
    public partial class HumanoidRetargeterDmx
    {
        /// <summary>
        /// A collection of <see cref="Element"/>s owned by a single <see cref="HumanoidRetargeterDmx"/>.
        /// </summary>
        [DebuggerDisplay("Count = {Count}")]
        [DebuggerTypeProxy(typeof(DebugView))]
        public class ElementList : IEnumerable<Element>, INotifyCollectionChanged, IDisposable
        {
            internal ReaderWriterLockSlim ChangeLock = new(LockRecursionPolicy.SupportsRecursion);

            internal class DebugView
            {
                public DebugView(ElementList item)
                {
                    Item = item;
                }

                private readonly ElementList Item;

                [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
                public Element[] Elements => Item.store.Values.Cast<Element>().ToArray();
            }

            private readonly OrderedDictionary store = [];
            private readonly HumanoidRetargeterDmx Owner;

            internal ElementList(HumanoidRetargeterDmx owner)
            {
                Owner = owner;
            }

            internal void Add(Element item)
            {
                ChangeLock.EnterUpgradeableReadLock();
                try
                {
                    Element? existing = (Element?)store[item.ID];
                    if (existing != null && !existing.Stub)
                    {
                        throw new ElementIdException($"Element ID {item.ID} already in use in this HumanoidRetargeterDmx.");
                    }

                    Debug.Assert(item.Owner != null);
                    if (item.Owner != this.Owner)
                        throw new ElementOwnershipException("Cannot add an element from a different HumanoidRetargeterDmx. Use ImportElement() to create a local copy instead.");

                    ChangeLock.EnterWriteLock();
                    try
                    {
                        if (existing != null)
                            store.Remove(existing.ID);

                        store.Add(item.ID, item);
                    }
                    finally
                    {
                        ChangeLock.ExitWriteLock();
                    }
                }
                finally
                {
                    ChangeLock.ExitUpgradeableReadLock();
                }

                CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
            }

            /// <summary>
            /// Returns the <see cref="Element"/> at the specified index.
            /// </summary>
            /// <remarks>The order of this list has no meaning to a HumanoidRetargeterDmx. This accessor is intended for <see cref="ICodec"/> implementers.</remarks>
            /// <param name="index">The index to look up.</param>
            /// <returns>The Element found at the index.</returns>
            public Element? this[int index]
            {
                get
                {
                    ChangeLock.EnterReadLock();
                    try
                    {
                        return (Element?)store[index];
                    }
                    finally { ChangeLock.ExitReadLock(); }
                }
            }

            /// <summary>
            /// Searches the collection for an <see cref="Element"/> with the specified <see cref="Element.ID"/>.
            /// </summary>
            /// <param name="id">The ID to search for.</param>
            /// <returns>The Element with the given ID, or null if none is found.</returns>
            public Element? this[Guid id]
            {
                get
                {
                    ChangeLock.EnterReadLock();
                    try
                    {
                        return (Element?)store[id];
                    }
                    finally { ChangeLock.ExitReadLock(); }
                }
            }

            /// <summary>
            /// Gets the number of <see cref="Element"/>s in this collection.
            /// </summary>
            public int Count
            {
                get
                {
                    ChangeLock.EnterReadLock();
                    try
                    {
                        return store.Count;
                    }
                    finally { ChangeLock.ExitReadLock(); }
                }
            }

            internal bool Contains(Element elem)
            {
                return store.Contains(elem);
            }

            /// <summary>
            /// Specifies a behaviour for removing references to an <see cref="Element"/> from other Elements in a <see cref="HumanoidRetargeterDmx"/>.
            /// </summary>
            public enum RemoveMode
            {
                /// <summary>
                /// Attribute values pointing to the removed Element become stubs.
                /// </summary>
                MakeStubs,
                /// <summary>
                /// Attribute values pointing to the removed Element become null.
                /// </summary>
                MakeNulls
            }
            /// <summary>
            /// Removes an <see cref="Element"/> from the HumanoidRetargeterDmx.
            /// </summary>
            /// <remarks>This method will access all values in the HumanoidRetargeterDmx, potentially triggering deferred loading and destubbing.</remarks>
            /// <param name="item">The Element to remove.</param>
            /// <param name="mode">The action to take if a reference to this Element is found in another Element.</param>
            /// <returns>true if item is successfully removed; otherwise, false.</returns>
            public bool Remove(Element item, RemoveMode mode)
            {
                ArgumentNullException.ThrowIfNull(item);

                ChangeLock.EnterUpgradeableReadLock();
                try
                {
                    if (store.Contains(item.ID))
                    {
                        ChangeLock.EnterWriteLock();
                        try
                        {
                            store.Remove(item.ID);
                            Element? replacement = (mode == RemoveMode.MakeStubs) ? new Element(Owner, item.ID) : null;

                            foreach (Element elem in store.Values)
                            {
                                lock (elem.SyncRoot)
                                {
                                    foreach (var attr in elem.Where(a => a.Value == item).ToArray())
                                    {
                                        elem[attr.Key] = replacement;
                                    }

                                    foreach (var array in elem.Select(a => a.Value).OfType<IList<Element?>>())
                                        for (int i = 0; i < array.Count; i++)
                                            if (array[i] == item)
                                                array[i] = replacement;
                                }
                            }
                            if (Owner.Root == item) Owner.Root = replacement;

                            item.Owner = null;
                        }
                        finally
                        {
                            ChangeLock.ExitWriteLock();
                        }

                        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
                        return true;
                    }
                    else return false;
                }
                finally { ChangeLock.ExitUpgradeableReadLock(); }
            }

            /// <summary>
            /// Removes unreferenced Elements from the HumanoidRetargeterDmx.
            /// </summary>
            public void Trim()
            {
                ChangeLock.EnterUpgradeableReadLock();
                try
                {
                    var used = new HashSet<Element?>();
                    WalkElemTree(Owner.Root, used);
                    if (used.Count == Count) return;

                    ChangeLock.EnterWriteLock();
                    try
                    {
                        foreach (var elem in this.Except(used).ToArray())
                        {
                            if(elem != null)
                            {
                                store.Remove(elem.ID);
                                elem.Owner = null;
                            }
                        }

                        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, (System.Collections.IList)used));
                    }
                    finally
                    {
                        ChangeLock.ExitWriteLock();
                    }
                }
                finally
                {
                    ChangeLock.ExitUpgradeableReadLock();
                }
            }

            protected void WalkElemTree(Element? elem, HashSet<Element?> found)
            {
                if(elem is null)
                {
                    return;
                }

                found.Add(elem);
                foreach (var value in elem.Inner.Values.Cast<Attribute>().Select(a => a.RawValue))
                {
                    if (value is Element value_elem)
                    {
                        if (found.Add(value_elem))
                        {
                            WalkElemTree(value_elem, found);
                        }

                        continue;
                    }
                    if (value is ElementArray elem_array)
                    {
                        foreach (var item in elem_array.RawList)
                        {
                            if (item != null && found.Add(item))
                            {
                                WalkElemTree(item, found);
                            }
                        }
                    }
                }
            }

            #region Interfaces
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return store.GetEnumerator();
            }
            /// <summary>
            /// Returns an Enumerator that iterates through the Elements in collection.
            /// </summary>
            public IEnumerator<Element> GetEnumerator()
            {
                foreach (Element elem in store.Values)
                    yield return elem;
            }
            /// <summary>
            /// Raised when an <see cref="Element"/> is added, removed, or replaced.
            /// </summary>
            public event NotifyCollectionChangedEventHandler? CollectionChanged;
            #endregion

            public void Dispose()
            {
                ChangeLock.Dispose();
            }
        }
    }
}