using System.Diagnostics;
using System.Text;
using Engine.Asset;
using Engine.Asset.Font;
using Engine.Asset.Mesh;
using Engine.Graphics;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Pixel;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using Engine.Input;
using Engine.Resource;
using Engine.Resource.Loader;
using Engine.Scene;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using Serilog;
using Debug = Engine.Graphics.Debug;

namespace Engine;

public sealed class Engine
{
  public static Engine Instance { get; private set; } = null!;

  public Renderer Renderer { get; }
  public SceneManager SceneManager { get; } = new();

  internal Window Window { get; }

  internal IPresenter? Presenter
  {
    get => _presenter;
    set
    {
      if (_presenter != null)
      {
        _presenter.Resize -= PresenterResize;
      }

      _presenter = value;

      if (_presenter != null)
      {
        _presenter.Resize += PresenterResize;
      }
    }
  }

  public IInputHandler? InputHandler
  {
    get => _inputHandler;
    internal set => _inputHandler = value;
  }

  private readonly ILogger _logger;

  private IInputHandler? _inputHandler;
  private IPresenter? _presenter;

  internal ResourceManager EngineResourceManager => _engineResourceManager;
  public IResourceManager AssetResourceManager => _assetResourceManager;
  private readonly ResourceManager _engineResourceManager;
  private readonly ResourceManager _assetResourceManager;

  private Thread? _updateThread;

  public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, string parAssetFolder,
    ILogger parLogger)
  {
    if (Instance != null)
    {
      throw new InvalidOperationException("Engine is already running");
    }

    Instance = this;

    Log.Logger = parLogger;
    _logger = Log.ForContext<Engine>();

    var settings = new NativeWindowSettings
    {
      ClientSize = parHeadless ? new Vector2i(1, 1) : new Vector2i(parWidth, parHeight),
      Title = parTitle,
      StartVisible = !parHeadless,
      APIVersion = new Version(4, 6),
      Profile = ContextProfile.Core,
      DepthBits = 0,
      StencilBits = 0
    };

    _engineResourceManager = CreateEngineResourceManager();
    _assetResourceManager = CreateAssetResourceManager(parAssetFolder);

    _logger.Information("Created asset resource manager in {AssetFolder}", parAssetFolder);

    Renderer = new Renderer(this, parWidth, parHeight, settings);
    Window = new Window(this, Renderer.NativeWindow, parHeadless);
  }

  private static ResourceManager CreateEngineResourceManager()
  {
    var memoryStreamProvider = new MemoryResourceStreamProvider();
    memoryStreamProvider.AddResource("shader/mesh", Encoding.UTF8.GetBytes(ShaderResource.Mesh));
    memoryStreamProvider.AddResource("shader/quad", Encoding.UTF8.GetBytes(ShaderResource.Quad));
    memoryStreamProvider.AddResource("shader/text", Encoding.UTF8.GetBytes(ShaderResource.Text));

    var resourceManager = new ResourceManager(memoryStreamProvider);
    RegisterDefaultLoaders(resourceManager);

    return resourceManager;
  }

  private static ResourceManager CreateAssetResourceManager(string parAssetFolder)
  {
    var filesystemStreamProvider = new FilesystemResourceStreamProvider(parAssetFolder);

    var resourceManager = new ResourceManager(filesystemStreamProvider);
    RegisterDefaultLoaders(resourceManager);

    return resourceManager;
  }

  private static void RegisterDefaultLoaders(ResourceManager parResourceManager)
  {
    parResourceManager.RegisterLoader<Program>(new ProgramLoader());
    parResourceManager.RegisterLoader<Mesh>(new MeshLoader());
    parResourceManager.RegisterLoader<Image<Rgba8>>(new ImageLoader());
    parResourceManager.RegisterLoader<Texture>(new TextureLoader());
    parResourceManager.RegisterLoader<Font>(new FontLoader());
  }

  private readonly object _sceneLock = new();

  public void Run()
  {
    _updateThread = new Thread(RunUpdate) { Name = "UpdateThread" };
    _updateThread.Start();

    RunRender();

    _updateThread.Join();
  }

  public void Close()
  {
    Presenter?.Exit();
  }

  private void RunRender()
  {
    while (!Presenter?.IsExiting ?? false)
    {
#if DEBUG
      Debug.RenderDocStartFrame();
#endif

      Renderer.RunScheduledActions();

      if (Monitor.TryEnter(_sceneLock))
      {
        Renderer.StartFrame();
        var matrices = new Dictionary<RenderLayer, (Matrix4, Matrix4)>();
        {
          var cameras = SceneManager.CurrentScene?.Cameras;
          if (cameras != null)
          {
            foreach (var (renderLayer, camera) in cameras)
            {
              camera.ScreenSize = new Vector2i(Renderer.ViewportWidth, Renderer.ViewportHeight);
              matrices[renderLayer] = (camera.Projection, camera.View);
            }
          }

          SceneManager.Render();
        }
        Monitor.Exit(_sceneLock);

        Renderer.EndFrame(matrices);

        Presenter!.Present(Renderer.RenderTexture);
        Presenter!.Render();
      }

#if DEBUG
      Debug.RenderDocEndFrame();
#endif
    }
  }

  private void RunUpdate()
  {
    var timer = Stopwatch.StartNew();
    var deltaTime = 0.0;

    while (!Presenter?.IsExiting ?? false)
    {
      Presenter!.Update(deltaTime);
      InputHandler?.Update(deltaTime);

      lock (_sceneLock)
      {
        try
        {
          SceneManager.Update(deltaTime);
        }
        catch (Exception ex)
        {
          _logger.Error(ex, "Exception in scene update");
        }
      }

      deltaTime = timer.Elapsed.TotalSeconds;
      timer.Restart();
    }
  }

  private void PresenterResize(ResizeEventArgs parEventArgs)
  {
    if (parEventArgs.Width == 0 || parEventArgs.Height == 0)
      return;

    Renderer.Resize(parEventArgs.Width, parEventArgs.Height);
  }
}