using System.Reflection;
using Engine.Scene.Component;
using Engine.Scene.Component.BuiltIn;

namespace Engine.Scene;

public sealed class GameObject : IUpdate, IRender
{
  public Guid Id { get; } = Guid.NewGuid();

  public bool IsEnabled
  {
    get => IsSelfEnabled && IsParentEnabled;
    set => IsSelfEnabled = value;
  }

  private bool IsSelfEnabled { get; set; } = true;
  private bool _prevIsSelfEnabled = true;
  private bool IsParentEnabled => Scene?.Hierarchy.GetParent(this)?.IsEnabled ?? true;

  public Transform Transform { get; }

  internal Scene? Scene { get; set; }

  private readonly Queue<Action> _componentActions = new();

  private readonly List<Component.Component> _components = [];
  private readonly HashSet<Type> _addedComponentTypes = [];

  public GameObject()
  {
    AddComponent<Transform>();
    ProcessChanges();

    Transform = GetComponent<Transform>()!;
  }

  public GameObject(Transform parTransform)
  {
    AddComponent(parTransform.Clone());
    ProcessChanges();

    Transform = GetComponent<Transform>()!;
  }

  public void Awake()
  {
    foreach (var component in _components)
    {
      component.Awake();
    }
  }

  public void Start()
  {
    foreach (var component in _components)
    {
      component.Start();
    }
  }

  public void Update(double parDeltaTime)
  {
    if (!IsEnabled)
    {
      if (!_prevIsSelfEnabled)
        return;

      foreach (var component in _components)
      {
        component.Disable();
      }

      _prevIsSelfEnabled = false;
      return;
    }

    if (!_prevIsSelfEnabled)
    {
      foreach (var component in _components)
      {
        component.Enable();
      }
    }

    _prevIsSelfEnabled = true;

    foreach (var component in _components)
    {
      component.Update(parDeltaTime);
    }
  }

  public void Render()
  {
    if (!IsEnabled)
      return;

    foreach (var component in _components)
    {
      component.Render();
    }
  }

  public void Destroy()
  {
    foreach (var component in _components)
    {
      component.Destroy();
    }
  }

  public T? GetComponent<T>() where T : Component.Component
  {
    return !HasComponent<T>() ? null : _components.OfType<T>().First();
  }

  public void AddComponent<T>() where T : Component.Component, new()
  {
    var component = new T();
    AddComponent(component);
  }

  public void AddComponent<T>(params object?[] parArgs) where T : Component.Component
  {
    var component = (T?)Activator.CreateInstance(
      typeof(T),
      BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance |
      BindingFlags.OptionalParamBinding,
      null,
      parArgs,
      null
    );

    if (component == null)
    {
      throw new InvalidOperationException($"Failed to create component of type {typeof(T)}");
    }

    AddComponent(component);
  }

  public void AddComponent<T>(T parComponent) where T : Component.Component
  {
    _componentActions.Enqueue(() =>
    {
      if (HasComponent<T>())
      {
        throw new ArgumentException($"GameObject already has component of type {typeof(T)}");
      }

      parComponent.GameObject = this;
      _components.Add(parComponent);
      _addedComponentTypes.Add(parComponent.GetType().GetComponentBaseType());
    });
  }

  public void RemoveComponent<T>() where T : Component.Component
  {
    if (typeof(T) == typeof(Transform))
    {
      throw new ArgumentException("GameObject cannot remove Transform component");
    }

    _componentActions.Enqueue(() =>
    {
      if (!HasComponent<T>())
      {
        throw new ArgumentException($"GameObject does not have component of type {typeof(T)}");
      }

      var component = GetComponent<T>();
      if (component == null)
      {
        return;
      }

      _components.Remove(component);
      _addedComponentTypes.Remove(typeof(T));
    });
  }

  public bool HasComponent<T>() where T : Component.Component
  {
    var baseType = typeof(T).GetComponentBaseType();
    return _addedComponentTypes.Contains(baseType);
  }

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