using Engine.Graphics.Camera;
using Engine.Graphics.Pipeline;
using Engine.Scene.Component.BuiltIn;

namespace Engine.Scene;

public class Scene : IUpdate, IRender
{
  public bool IsPlaying { get; private set; }
  public IReadOnlyDictionary<RenderLayer, ICamera> Cameras => _cameras;
  private readonly Dictionary<RenderLayer, ICamera> _cameras = new();

  internal Hierarchy<GameObject> Hierarchy { get; } = new();

  private readonly Queue<Action> _sceneActions = [];

  internal void Enter()
  {
    if (IsPlaying)
    {
      throw new InvalidOperationException("Scene is already playing");
    }

    ProcessChanges();

    var allCameras = FindAllComponents<Camera>();
    foreach (var camera in allCameras)
    {
      _cameras.Add(camera.RenderLayer, camera);
    }

    IsPlaying = true;
  }

  public List<T> FindAllComponents<T>() where T : Component.Component
  {
    return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>())
      .Where(parComponent => parComponent != null).ToList()!;
  }

  public T? FindFirstComponent<T>() where T : Component.Component
  {
    return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>())
      .FirstOrDefault(parComponent => parComponent != null);
  }

  public void Update(double parDeltaTime)
  {
    if (!IsPlaying)
    {
      throw new InvalidOperationException("Scene is not playing");
    }

    ProcessChanges();

    foreach (var gameObject in Hierarchy.Objects)
    {
      gameObject.Update(parDeltaTime);
    }
  }

  public void Render()
  {
    if (!IsPlaying)
    {
      throw new InvalidOperationException("Scene is not playing");
    }

    foreach (var gameObject in Hierarchy.Objects)
    {
      gameObject.Render();
    }
  }

  internal void Exit()
  {
    if (!IsPlaying)
    {
      throw new InvalidOperationException("Scene is not playing");
    }

    foreach (var gameObject in Hierarchy.Objects)
    {
      gameObject.Destroy();
    }

    IsPlaying = false;
  }

  public void Add(GameObject parGameObject)
  {
    Hierarchy.Add(parGameObject);

    _sceneActions.Enqueue(() =>
    {
      parGameObject.Scene = this;

      parGameObject.Awake();
      parGameObject.Start();
    });
  }

  public void AddChild(GameObject parParent, GameObject parChild)
  {
    Add(parChild);
    Hierarchy.AddChild(parParent, parChild);
  }

  public void Remove(GameObject parGameObject)
  {
    Hierarchy.Remove(parGameObject);

    _sceneActions.Enqueue(() =>
    {
      foreach (var child in Hierarchy.GetAllChildren(parGameObject))
      {
        child.Destroy();
        child.Scene = null;
      }

      parGameObject.Destroy();

      parGameObject.Scene = null;
    });
  }

  private void ProcessChanges()
  {
    Hierarchy.ProcessChanges();

    foreach (var gameObject in Hierarchy.Objects)
    {
      gameObject.ProcessChanges();
    }

    while (_sceneActions.TryDequeue(out var action))
    {
      action();
    }
  }
}