Editor/HumanoidRetargeter/Embedded/Datamodel/AttributeList.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq;
using System.Numerics;

using AttrKVP = System.Collections.Generic.KeyValuePair<string, object?>;
using System.Reflection;
using System.IO;

namespace HumanoidRetargeterDmx
{
    /// <summary>
    /// A thread-safe collection of <see cref="Attribute"/>s.
    /// </summary>
    [DebuggerTypeProxy(typeof(DebugView))]
    [DebuggerDisplay("Count = {Count}")]
    public class AttributeList : IDictionary<string, object?>, IDictionary
    {
        internal OrderedDictionary PropertyInfos;
        internal OrderedDictionary Inner;
        protected object Attribute_ChangeLock = new();

        private ICollection<Attribute> GetPropertyBasedAttributes(bool useSerializationName)
        {
            var result = new List<Attribute>();
            foreach (DictionaryEntry entry in PropertyInfos)
            {
                if(entry.Value is null)
                {
                    throw new InvalidDataException("Property value can not be null");
                }

                var prop = (PropertyInfo)entry.Value;
                var name = useSerializationName ? (string)entry.Key : prop.Name;
                var attr = new Attribute(name, this, prop.GetValue(this));
                result.Add(attr);
            }
            return result;
        }

        internal class DebugView
        {
            public DebugView(AttributeList item)
            {
                Item = item;
            }
            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            protected AttributeList Item;

            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            public DebugAttribute[] Attributes
                => Item.GetPropertyBasedAttributes(useSerializationName: false).Select(attr => new DebugAttribute(attr))
                .Concat(Item.Inner.Values.Cast<Attribute>().Select(attr => new DebugAttribute(attr)))
                .ToArray();

            [DebuggerDisplay("{Value}", Name = "{Attr.Name,nq}", Type = "{Attr.ValueType.FullName,nq}")]
            public class DebugAttribute
            {
                public DebugAttribute(Attribute attr)
                {
                    Attr = attr;
                }

                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
                readonly Attribute Attr;

                [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
                object? Value { get { return Attr.Value; } }
            }
        }

        /// <summary>
        /// Contains the names of HumanoidRetargeterDmx types which are functionally identical to other types and don't have their own CLR representation.
        /// </summary>
        public enum OverrideType
        {
            /// <summary>
            /// Maps to <see cref="global::System.Numerics.Vector3"/>.
            /// </summary>
            Angle,
            /// <summary>
            /// Maps to <see cref="byte[]"/>.
            /// </summary>
            Binary,
        }

        public AttributeList(HumanoidRetargeterDmx? owner)
        {
            var propertyAttributes = GetPropertyDerivedAttributeList();
            PropertyInfos = new OrderedDictionary(propertyAttributes?.Count ?? 0);
            if (propertyAttributes != null)
            {
                foreach (var attr in propertyAttributes)
                {
                    PropertyInfos.Add(attr.Name, attr.Property);
                }
            }

            Inner = [];
            Owner = owner;
        }

        /// <summary>
        /// Gets the <see cref="HumanoidRetargeterDmx"/> that this AttributeList is owned by.
        /// </summary>
        public virtual HumanoidRetargeterDmx? Owner { get; internal set; }

        /// <summary>
        /// Adds a new attribute to this AttributeList.
        /// </summary>
        /// <param name="key">The name of the attribute. Must be unique to this AttributeList.</param>
        /// <param name="value">The value of the Attribute. Must be of a valid HumanoidRetargeterDmx type.</param>
        public void Add(string key, object? value)
        {
            this[key] = value;
        }


        protected virtual ICollection<(string Name, PropertyInfo Property)>? GetPropertyDerivedAttributeList()
        {
            return null;
        }

        /// <summary>
        /// Gets the given atttribute's "override type". This applies when multiple HumanoidRetargeterDmx types map to the same CLR type.
        /// </summary>
        /// <param name="key">The name of the attribute.</param>
        /// <returns>The attribute's HumanoidRetargeterDmx type, if different from its CLR type.</returns>
        /// <exception cref="KeyNotFoundException">Thrown when the given attribute is not present in the list.</exception>
        public OverrideType? GetOverrideType(string key)
        {
            var attrib = Inner[key];

            if(attrib is null)
            {
                return null;
            }

            return ((Attribute)attrib).OverrideType;
        }

        /// <summary>
        /// Sets the given attribute's "override type". This applies when multiple HumanoidRetargeterDmx types map to the same CLR type.
        /// </summary>
        /// <param name="key">The name of the attribute.</param>
        /// <param name="type">The HumanoidRetargeterDmx type which the attribute should be stored as when written to DMX, or null.</param>
        /// <exception cref="AttributeTypeException">Thrown when the attribute's CLR type does not map to the value given in <paramref name="type"/>.</exception>
        public void SetOverrideType(string key, OverrideType? type)
        {
            var attrib = Inner[key];

            if (attrib is not null)
            {
                ((Attribute)attrib).OverrideType = type;
            }

        }

        /// <summary>
        /// Inserts an Attribute at the given index.
        /// </summary>
        private void Insert(int index, Attribute item, bool notify = true)
        {
            lock (Attribute_ChangeLock)
            {
                Inner.Remove(item.Name);
                Inner.Insert(index, item.Name, item);
            }
            item.Owner = this;

            if (notify)
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item.ToKeyValuePair(), index));
        }

        public bool Remove(string key)
        {
            lock (Attribute_ChangeLock)
            {
                var attr = (Attribute?)Inner[key];
                if (attr == null) return false;

                var index = IndexOf(key);
                Inner.Remove(key);
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, attr.ToKeyValuePair(), index));
                return true;
            }
        }

        public bool TryGetValue(string key, out object? value)
        {
            Attribute? result;
            lock (Attribute_ChangeLock)
                result = (Attribute?)Inner[key];

            if (result != null)
            {
                value = result.RawValue;
                return true;
            }
            else
            {
                value = null;
                return false;
            }
        }

        public virtual bool ContainsKey(string key)
        {
            ArgumentNullException.ThrowIfNull(key);
            lock (Attribute_ChangeLock)
                return Inner[key] != null;
        }
        public ICollection<string> Keys
        {
            get { lock (Attribute_ChangeLock) return Inner.Keys.Cast<string>().ToArray(); }
        }
        public ICollection<object?> Values
        {
            get { lock (Attribute_ChangeLock) return Inner.Values.Cast<Attribute>().Select(attr => attr.Value).ToArray(); }
        }

        /// <summary>
        /// Gets or sets the value of the <see cref="Attribute"/> with the given name.
        /// </summary>
        /// <param name="name">The name to search for. Cannot be null.</param>
        /// <returns>The value associated with the given name.</returns>
        /// <exception cref="ArgumentNullException">Thrown when the value of name is null.</exception>
        /// <exception cref="KeyNotFoundException">Thrown when an attempt is made to get a name that is not present in this AttributeList.</exception>
        /// <exception cref="ElementOwnershipException">Thrown when an attempt is made to set the value of the attribute to an Element from a different <see cref="HumanoidRetargeterDmx"/>.</exception>
        /// <exception cref="AttributeTypeException">Thrown when an attempt is made to set a value that is not of a valid HumanoidRetargeterDmx attribute type.</exception>
        /// <exception cref="IndexOutOfRangeException">Thrown when the maximum number of Attributes allowed in an AttributeList has been reached.</exception>        
        public virtual object? this[string name]
        {
            get
            {
                ArgumentNullException.ThrowIfNull(name);
                var attr = (Attribute?)Inner[name];
                if (attr == null)
                {
                    var prop_attr = (PropertyInfo?)PropertyInfos[name];
                    if (prop_attr != null)
                    {
                        return prop_attr.GetValue(this);
                    }

                    throw new KeyNotFoundException($"{this} does not have an attribute called \"{name}\"");
                }

                return attr.Value;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(name);
                if (value != null && !HumanoidRetargeterDmx.IsHumanoidRetargeterDmxType(value.GetType()))
                    throw new AttributeTypeException($"{value.GetType().FullName} is not a valid HumanoidRetargeterDmx attribute type. (If this is an array, it must implement IList<T>).");

                if (Owner != null && this == Owner.PrefixAttributes && value?.GetType() == typeof(Element))
                    throw new AttributeTypeException("Elements are not supported as prefix attributes.");

                var prop = (PropertyInfo?)PropertyInfos[name];

                if (prop != null)
                {
                    if (prop.CanWrite)
                    {
                        // were actually fine with this being null, it will just set the value to null
                        // but need to check so the type check doesn't fail if it is null
                        if(value != null)
                        {
                            var valueType = value.GetType();

                            // types must be equal, or a superclass
                            if (prop.PropertyType != typeof(Element) && valueType.IsSubclassOf(prop.PropertyType))
                            {
                                throw new InvalidDataException($"class property '{prop.Name}' with type '{prop.PropertyType}' does not match the type '{valueType}' of the value being set, this is likely a mismatch between the real class and the class from the datamodel");
                            }
                        }

                        prop.SetValue(this, value);
                    }
                    else
                    {
                        var existingArray = prop.GetValue(this) as Array<Element>;
                        var incomingArray = value as Array<Element>;

                        if (existingArray is not null && incomingArray is not null)
                        {
                            // special case for reflection based deserialization
                            if (existingArray.Count == 0)
                            {
                                existingArray.AddRange(incomingArray);
                                return;
                            }
                            else
                            {
                                throw new InvalidOperationException($"Attribute '{name}' modifies property {prop.DeclaringType!.Name}.{prop.Name}, which is write only and can't be replaced.");
                            }
                        }


                        throw new InvalidDataException("Property of deserialisation class must be writeable, make sure it's public and has a public setter");
                    }

                    return;
                }
                
                Attribute? old_attr;
                Attribute? new_attr;
                int old_index = -1;
                lock (Attribute_ChangeLock)
                {
                    old_attr = (Attribute?)Inner[name];
                    new_attr = new Attribute(name, this, value);

                    if (old_attr != null)
                    {
                        old_index = IndexOf(old_attr.Name);
                        Inner.Remove(old_attr);
                    }
                    Insert(old_index == -1 ? Count : old_index, new Attribute(name, this, value), notify: false);
                }

                NotifyCollectionChangedEventArgs change_args;
                if (old_attr != null)
                    change_args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, new_attr.ToKeyValuePair(), old_attr.ToKeyValuePair(), old_index);
                else
                    change_args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new_attr.ToKeyValuePair(), Count);

                OnCollectionChanged(change_args);
            }
        }

        /// <summary>
        /// Gets or sets the attribute at the given index.
        /// </summary>
        public AttrKVP this[int index]
        {
            get
            {
                var attr = (Attribute?)Inner[index];

                if(attr is null)
                {
                    throw new InvalidOperationException($"attribute at index {index} doesn't exist");
                }

                return attr.ToKeyValuePair();
            }
            set
            {
                RemoveAt(index);
                Insert(index, new Attribute(value.Key, this, value.Value));
            }
        }

        /// <summary>
        /// Removes the attribute at the given index.
        /// </summary>
        public void RemoveAt(int index)
        {
            Attribute? attr;
            lock (Attribute_ChangeLock)
            {
                attr = (Attribute?)Inner[index];

                if(attr is not null)
                {
                    attr.Owner = null;
                    Inner.RemoveAt(index);
                }
            }
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, attr, index));
        }

        public int IndexOf(string key)
        {
            lock (Attribute_ChangeLock)
            {
                int i = 0;
                foreach (string name in Inner.Keys)
                {
                    if (name == key) return i;
                    i++;
                }
            }
            return -1;
        }

        /// <summary>
        /// Removes all Attributes from the Collection.
        /// </summary>
        public void Clear()
        {
            lock (Attribute_ChangeLock)
                Inner.Clear();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        public int Count
        {
            get
            {
                lock (Attribute_ChangeLock)
                    return Inner.Count;
            }
        }

        public bool IsFixedSize { get { return false; } }
        public bool IsReadOnly { get { return false; } }
        public bool IsSynchronized { get { return true; } }
        /// <summary>
        /// Gets an object which can be used to synchronise access to the items within this AttributeCollection.
        /// </summary>
        public object SyncRoot { get { return Attribute_ChangeLock; } }

        public IEnumerable<AttrKVP> GetAllAttributesForSerialization()
        {
            foreach (var attr in GetPropertyBasedAttributes(useSerializationName: true))
                yield return attr.ToKeyValuePair();

            foreach (var attr in this)
                yield return attr;
        }

        public IEnumerator<AttrKVP> GetEnumerator()
        {
            foreach (var attr in Inner.Values.Cast<Attribute>().ToArray())
                yield return attr.ToKeyValuePair();
        }

        #region Interfaces

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }


        /// <summary>
        /// Raised when <see cref="Element.Name"/>, <see cref="Element.ClassName"/>, <see cref="Element.ID"/> or
        /// a custom Element property has changed.
        /// </summary>
        public event PropertyChangedEventHandler? PropertyChanged;
        protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName()] string property = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }

        /// <summary>
        /// Raised when an <see cref="Attribute"/> is added, removed, or replaced.
        /// </summary>
        public event NotifyCollectionChangedEventHandler? CollectionChanged;
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            Debug.Assert(!(e.NewItems != null && e.NewItems.OfType<Attribute>().Any()) && !(e.OldItems != null && e.OldItems.OfType<Attribute>().Any()));

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                case NotifyCollectionChangedAction.Reset:
                    OnPropertyChanged("Count");
                    break;
            }

            OnPropertyChanged("Item[]"); // this is the magic value of System.Windows.Data.Binding.IndexerName that tells the binding engine an indexer has changed

            CollectionChanged?.Invoke(this, e);
        }


        IDictionaryEnumerator IDictionary.GetEnumerator()
        {
            throw new NotImplementedException();
        }

        void IDictionary.Remove(object key)
        {
            Remove((string)key);
        }

        void IDictionary.Add(object key, object? value)
        {
            Add((string)key, value);
        }

        object? IDictionary.this[object key]
        {
            get
            {
                return this[(string)key];
            }
            set
            {
                this[(string)key] = value;
            }
        }

        bool IDictionary.Contains(object key)
        {
            return ContainsKey((string)key);
        }

        ICollection IDictionary.Keys { get { return (ICollection)Keys; } }
        ICollection IDictionary.Values { get { return (ICollection)Values; } }

        bool ICollection<AttrKVP>.Remove(AttrKVP item)
        {
            lock (Attribute_ChangeLock)
            {
                var attr = (Attribute?)Inner[item.Key];
                if (attr == null || attr.Value != item.Value) return false;
                Remove(attr.Name);
                return true;
            }
        }

        void ICollection<AttrKVP>.CopyTo(AttrKVP[] array, int arrayIndex)
        {
            ((ICollection)this).CopyTo(array, arrayIndex);
        }

        void ICollection.CopyTo(Array array, int index)
        {
            lock (Attribute_ChangeLock)
                foreach (Attribute attr in Inner.Values)
                {
                    array.SetValue(attr.ToKeyValuePair(), index);
                    index++;
                }
        }

        void ICollection<AttrKVP>.Add(AttrKVP item)
        {
            this[item.Key] = item.Value;
        }

        bool ICollection<AttrKVP>.Contains(AttrKVP item)
        {
            lock (Attribute_ChangeLock)
            {
                var attr = (Attribute?)Inner[item.Key];
                return attr != null && attr.Value == item.Value;
            }
        }

        #endregion
    }

    /// <summary>
    /// Compares two Attribute values, using <see cref="Element.IDComparer"/> for AttributeList comparisons.
    /// </summary>
    public class ValueComparer : IEqualityComparer
    {
        /// <summary>
        /// Gets a default Attribute value equality comparer.
        /// </summary>
        public static ValueComparer Default
        {
            get
            {
                _Default ??= new ValueComparer();
                return _Default;
            }
        }
        static ValueComparer? _Default;

        public new bool Equals(object? x, object? y)
        {
            if(x is null || y is null)
            {
                return false;
            }

            var type_x = x.GetType();
            var type_y = y.GetType();

            if (type_x == null && type_y == null)
                return true;

            if (type_x != type_y)
                return false;

            var inner = HumanoidRetargeterDmx.GetArrayInnerType(type_x);
            if (inner != null)
            {
                var array_left = (IList)x;
                var array_right = (IList)y;

                if (array_left.Count != array_right.Count) return false;

                return !Enumerable.Range(0, array_left.Count).Any(i => !Equals(array_left[i], array_right[i]));
            }
            else if (type_x == typeof(Element))
                return Element.IDComparer.Default.Equals((Element)x, (Element)y);
            else
                return EqualityComparer<object>.Default.Equals(x, y);
        }

        public int GetHashCode(object obj)
        {
            if (obj is Element elem)
                return elem.ID.GetHashCode();

            var inner = HumanoidRetargeterDmx.GetArrayInnerType(obj.GetType());
            if (inner != null)
            {
                int hash = 0;
                foreach (var item in (IList)obj)
                    hash ^= item.GetHashCode();
                return hash;
            }

            return obj.GetHashCode();
        }
    }
}