Code/Util/DynamicDirectory.cs
namespace Nodebox;

public class DynamicDirectory<TBase> : IDynamicDirectory<TBase> where TBase : class, DynamicDirectory<TBase>.IElement {
    private Dictionary<Type, BiDictionary<Guid, TBase>> _store = [];

    internal BiDictionary<Guid, TBase> GetStore(Type type) {
        Assert.AreEqual(TypeLibrary.GetType(type).BaseType.TargetType, typeof(TBase), $"{type} has to be a direct descendant of {nameof(TBase)}");
        return _store.GetOrCreate(type);
    }

    internal BiDictionary<Guid, TBase> GetStore<T>() => GetStore(typeof(T));

    public int CountOf(Type type) => GetStore(type).Count;
    public int CountOf<T>() => CountOf(typeof(T));

    public IEnumerable<TBase> GetAll(Type type) => GetStore(type).Reverse.Keys;
    public IEnumerable<T> GetAll<T>() where T : TBase => GetAll(typeof(T)).Select(x => (T)x);

    private bool IsFreeId(Guid id) => _store.All(x => !x.Value.Forward.ContainsKey(id));

    private Guid UniqueGuid() {
        var guid = Guid.NewGuid();
        while (!IsFreeId(guid)) {
            guid = Guid.NewGuid();
        }
        return guid;
    }


    private void TBaseOnDestroy(TBase element) {
        Remove(element.DirectoryType, element);
    }

    public T Add<T>(T element) where T : TBase {
        // Log.Info(element);
        var store = GetStore(element.DirectoryType);
        if (store.Reverse.ContainsKey(element)) { return element; }
        store.Add(UniqueGuid(), element);
        element.OnDestroy += TBaseOnDestroy;
        return element;
    }

    public T Add<T>(T element, Guid previous) where T : TBase {
        var store = GetStore(element.DirectoryType);
        if (store.Forward.TryGetValue(previous, out var existing) && existing.Equals(element)) {
            store.Remove(previous);
        }

        return Add(element);
    }

    public void AddRange<T>(IEnumerable<T> elements) where T : TBase {
        foreach (var element in elements) {
            Add(element);
        }
    }

    public bool Remove(Type type, TBase element) {
        var success = GetStore(type).RemoveReverse(element);
        if (success) {
            element.OnDestroy -= TBaseOnDestroy;
        }

        return success;
    }

    public bool Remove<T>(T element) where T : TBase => Remove(typeof(T), element);
    public bool RemoveByGuid(Type type, Guid guid) {
        if (!GetStore(type).Forward.TryGetValue(guid, out var element)) {
            return false;
        }

        Remove(type, element);
        element.OnDestroy -= TBaseOnDestroy;

        return true;
    }
    public bool RemoveByGuid<T>(Guid guid) where T : TBase => RemoveByGuid(typeof(T), guid);


    public TBase FindByGuid(Type type, Guid guid) {
        if (GetStore(type).Forward.TryGetValue(guid, out var found)) {
            return found;
        }

        return null;
    }
    public T FindByGuid<T>(Guid guid) where T : TBase => (T)FindByGuid(typeof(T), guid);

    public Guid? Find(Type type, TBase element) {
        if (GetStore(type).Reverse.TryGetValue(element, out var found)) {
            return found;
        }

        return null;
    }
    public Guid? Find<T>(TBase element) where T : TBase => Find(typeof(T), element);


    public bool Contains(Type type, TBase element) => Find(type, element).HasValue;
    public bool Contains<T>(TBase element) where T : TBase => Find<T>(element).HasValue;

    public void Clear() {
        _store.Clear();
    }

    public interface IElement {
        public Type DirectoryType { get; }
        public Action<TBase> OnDestroy { get; set; }
    }
}

public interface IDynamicDirectory<TBase> {
    public int CountOf(Type type);
    public int CountOf<T>();

    public IEnumerable<TBase> GetAll(Type type);
    public IEnumerable<T> GetAll<T>() where T : TBase;

    public T Add<T>(T element) where T : TBase;
    public T Add<T>(T element, Guid previous) where T : TBase;
    public void AddRange<T>(IEnumerable<T> elements) where T : TBase;

    public bool Remove(Type type, TBase element);
    public bool Remove<T>(T element) where T : TBase;
    public bool RemoveByGuid(Type type, Guid guid);
    public bool RemoveByGuid<T>(Guid guid) where T : TBase;


    public TBase FindByGuid(Type type, Guid guid);
    public T FindByGuid<T>(Guid guid) where T : TBase;

    public Guid? Find(Type type, TBase element);
    public Guid? Find<T>(TBase element) where T : TBase;


    public bool Contains(Type type, TBase element);
    public bool Contains<T>(TBase element) where T : TBase;

    public void Clear();
}


public static class DynamicDirectoryIElementExtensions {
    extension<T>(DynamicDirectory<T>.IElement element) where T : class, DynamicDirectory<T>.IElement {
        public Action<T> OnDestroy {
            get => element.OnDestroy;
            set => element.OnDestroy = value;
        }
    }
}