using Engine.Graphics.Pipeline;
using Engine.Graphics.Pixel;
using Engine.Graphics.Render.Quad;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Desktop;

namespace Engine.Graphics;

public class Renderer
{
  internal Framebuffer.Framebuffer RenderFramebuffer => _framebuffer;
  internal Texture.Texture RenderTexture => _framebuffer.TextureInternal!;

  private QuadRenderer QuadRenderer { get; }
  public int ViewportWidth => _framebuffer.Width;
  public int ViewportHeight => _framebuffer.Height;

  private readonly SortedDictionary<RenderLayer, GenericRenderer> _renderers = new();

  public GenericRenderer this[RenderLayer parRenderLayer]
  {
    get
    {
      if (_renderers.TryGetValue(parRenderLayer, out var renderer))
        return renderer;

      throw new InvalidOperationException($"Renderer for layer {parRenderLayer} not found");
    }
  }

  internal NativeWindow NativeWindow { get; }

  private readonly Framebuffer.Framebuffer _framebuffer;
  private readonly Thread _renderThread;

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

  public Renderer(Engine parEngine, int parWidth, int parHeight, NativeWindowSettings parSettings)
  {
    _renderThread = Thread.CurrentThread;
    _renderThread.Name = "RendererThread";

#if DEBUG
    Debug.InitializeRenderDoc();
#endif

    NativeWindow = new NativeWindow(parSettings);

    InitializeOpenGl(parWidth, parHeight);

    _framebuffer = Framebuffer.Framebuffer.Builder(parWidth, parHeight)
      .AddColorAttachment<Rgba8>()
      .Build();

    QuadRenderer = new QuadRenderer(parEngine, 1);

    foreach (var layer in RenderLayer.ALL)
    {
      _renderers.Add(layer, new GenericRenderer(parEngine, parWidth, parHeight));
    }
  }

  private void InitializeOpenGl(int parWidth, int parHeight)
  {
#if DEBUG
    Debug.Setup();
#endif

    GL.Enable(EnableCap.DepthTest);
    // GL.Enable(EnableCap.CullFace);

    // GL.Enable(EnableCap.FramebufferSrgb);

    GL.Enable(EnableCap.Blend);
    GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);

    GL.Viewport(0, 0, parWidth, parHeight);
  }

  public void EnsureRenderThread()
  {
#if DEBUG
    if (Thread.CurrentThread != _renderThread)
    {
      throw new InvalidOperationException("Not on render thread");
    }
#endif
  }

  internal void Schedule(Action parAction)
  {
    _scheduleActions.Enqueue(parAction);
  }

  internal Task<T> Schedule<T>(Func<T> parAction)
  {
    var completionSource = new TaskCompletionSource<T>();

    Schedule(() =>
    {
      try
      {
        completionSource.SetResult(parAction());
      }
      catch (Exception ex)
      {
        completionSource.SetException(ex);
      }
    });

    return completionSource.Task;
  }

  internal void StartFrame()
  {
    EnsureRenderThread();

    foreach (var renderer in _renderers.Values)
    {
      renderer.StartFrame();
    }
  }

  internal void EndFrame(Dictionary<RenderLayer, (Matrix4, Matrix4)> parMatrices)
  {
    EnsureRenderThread();

    foreach (var (renderLayer, renderer) in _renderers)
    {
      if (!parMatrices.TryGetValue(renderLayer, out var matrices))
        renderer.EndFrame(Matrix4.Identity, Matrix4.Identity);
      else
        renderer.EndFrame(in matrices.Item1, in matrices.Item2);
    }

    _framebuffer.Bind();

    // GL.Disable(EnableCap.DepthTest);
    // GL.Disable(EnableCap.CullFace);

    GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    GL.Clear(ClearBufferMask.ColorBufferBit);

    foreach (var (renderLayer, renderer) in _renderers)
    {
      if (!parMatrices.ContainsKey(renderLayer))
        continue;

      QuadRenderer.Commit(Matrix4.CreateScale(2f, -2f, 1f), Vector4.One, renderer._framebuffer.TextureInternal);
      QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity);
      QuadRenderer.Reset();
    }

    // GL.Enable(EnableCap.DepthTest);
    // GL.Enable(EnableCap.CullFace);

    _framebuffer.Unbind();
  }

  internal void Resize(int parWidth, int parHeight)
  {
    _framebuffer.Resize(parWidth, parHeight);
    GL.Viewport(0, 0, parWidth, parHeight);

    foreach (var renderer in _renderers.Values)
    {
      renderer.Resize(parWidth, parHeight);
    }
  }

  internal void RunScheduledActions()
  {
    while (_scheduleActions.TryDequeue(out var action))
    {
      action();
    }
  }
}