using System.Collections.Concurrent;
using Engine.Util;

namespace Engine.Scene;

public class Hierarchy<T>
  where T : class
{
  internal Dictionary<T, T?>.KeyCollection Objects => _parentLookup.Keys;

  private readonly Dictionary<NullableObject<T>, IList<T>> _childrenLookup = new();
  private readonly Dictionary<T, T?> _parentLookup = new();

  private readonly ConcurrentQueue<Action> _hierarchyActions = new();

  public Hierarchy()
  {
    _childrenLookup.Add(new NullableObject<T>(), new List<T>());
  }

  internal void ProcessChanges()
  {
    while (_hierarchyActions.TryDequeue(out var action))
    {
      action();
    }
  }

  public void Add(T parObj)
  {
    _hierarchyActions.Enqueue(() =>
    {
      if (_parentLookup.ContainsKey(parObj))
      {
        return;
      }

      _childrenLookup.Add(parObj, new List<T>());
      _parentLookup.Add(parObj, null);
      _childrenLookup[null].Add(parObj);
    });
  }

  public void Remove(T parObj)
  {
    foreach (var child in GetChildren(parObj))
    {
      Remove(child);
    }

    _hierarchyActions.Enqueue(() =>
    {
      if (!Contains(parObj))
        return;

      var parent = GetParent(parObj);
      _childrenLookup[parent].Remove(parObj);

      _parentLookup.Remove(parObj);
      _childrenLookup.Remove(parObj);
    });
  }

  public void AddChild(T parParent, T parChild)
  {
    SetParent(parChild, parParent);
  }

  private void SetParent(T parChild, T? parParent)
  {
    if (parChild.Equals(parParent))
    {
      throw new InvalidOperationException("Child cannot be parent");
    }

    _hierarchyActions.Enqueue(() =>
    {
      if (IsInHierarchy(parChild, parParent))
      {
        throw new InvalidOperationException("Parent is a child of child");
      }

      var oldParent = GetParent(parChild);
      _childrenLookup[oldParent].Remove(parChild);

      _childrenLookup[parParent].Add(parChild);
      _parentLookup[parChild] = parParent;
    });
  }

  public bool Contains(T parObj)
  {
    return _parentLookup.ContainsKey(parObj) && _childrenLookup.ContainsKey(parObj);
  }

  public T? GetParent(T parChild)
  {
    return _parentLookup.TryGetValue(parChild, out var parent)
      ? parent
      : throw new InvalidOperationException($"Child {parChild} is not in hierarchy");
  }

  public IEnumerable<T> GetChildren(T? parParent = null)
  {
    return _childrenLookup.TryGetValue(parParent, out var children) ? children : Enumerable.Empty<T>();
  }

  public IEnumerable<T> GetAllChildren(T? parParent = null)
  {
    var children = GetChildren(parParent);

    foreach (var child in children)
    {
      foreach (var descendant in GetAllChildren(child))
      {
        yield return descendant;
      }

      yield return child;
    }
  }

  public bool IsInHierarchy(T? parAncestor, T? parChild)
  {
    if (parChild == null) // if child is null (root), then it is not in hierarchy, as root can not have a parent
    {
      return false;
    }

    if (parAncestor == null) // if ancestor is null (root), then child is not in hierarchy, as root is not a parent
    {
      return false;
    }

    if (parAncestor.Equals(parChild))
    {
      return true;
    }

    var parent = GetParent(parChild);

    if (parent == null)
    {
      return false;
    }

    if (parAncestor.Equals(parent))
    {
      return true;
    }

    return IsInHierarchy(parAncestor, parent);
  }
}