#pragma once

#include <array>
#include <memory>

#include "common/dma/dma_chain_read.h"

#include "game/graphics/opengl_renderer/BucketRenderer.h"
#include "game/graphics/opengl_renderer/CollideMeshRenderer.h"
#include "game/graphics/opengl_renderer/Profiler.h"
#include "game/graphics/opengl_renderer/Shader.h"
#include "game/graphics/opengl_renderer/opengl_utils.h"
#include "game/tools/subtitles/subtitle_editor.h"

struct RenderOptions {
  bool draw_render_debug_window = false;
  bool draw_profiler_window = false;
  bool draw_small_profiler_window = false;
  bool draw_subtitle_editor_window = false;

  // internal rendering settings - The OpenGLRenderer will internally use this resolution/format.
  int msaa_samples = 4;
  int game_res_w = 640;
  int game_res_h = 480;

  // size of the window's framebuffer (framebuffer 0)
  // The renderer needs to know this to do an optimization to render directly to the window's
  // framebuffer when possible.
  int window_framebuffer_height = 0;
  int window_framebuffer_width = 0;

  // the part of the window that we should draw to. The rest is black. This value is determined by
  // logic inside of the game - it needs to know the desired aspect ratio.
  int draw_region_height = 0;
  int draw_region_width = 0;

  // windows-specific tweaks to the size of the drawing area in borderless.
  bool borderless_windows_hacks = false;

  bool save_screenshot = false;
  std::string screenshot_path;

  float pmode_alp_register = 0.f;

  // when enabled, does a `glFinish()` after each major rendering pass. This blocks until the GPU
  // is done working, making it easier to profile GPU utilization.
  bool gpu_sync = false;
};

struct Fbo {
  bool valid = false;  // do we have an OpenGL fbo_id?
  GLuint fbo_id = -1;

  // optional rgba/zbuffer/stencil data.
  std::optional<GLuint> tex_id;
  std::optional<GLuint> zbuf_stencil_id;

  bool multisampled = false;
  int multisample_count = 0;  // Should be 1 if multisampled is disabled

  bool is_window = false;
  int width = 640;
  int height = 480;

  // Does this fbo match the given format? MSAA = 1 will accept a normal buffer, or a multisample 1x
  bool matches(int w, int h, int msaa) const {
    int effective_msaa = multisampled ? multisample_count : 1;
    return valid && width == w && height == h && effective_msaa == msaa;
  }

  // Free opengl resources, if we have any.
  void clear() {
    if (valid) {
      glDeleteFramebuffers(1, &fbo_id);
      fbo_id = -1;

      if (tex_id) {
        glDeleteTextures(1, &tex_id.value());
        tex_id.reset();
      }

      if (zbuf_stencil_id) {
        glDeleteRenderbuffers(1, &zbuf_stencil_id.value());
        zbuf_stencil_id.reset();
      }

      valid = false;
    }
  }
};

/*!
 * Main OpenGL renderer.
 * This handles the glClear and all game rendering, but not actual setup, synchronization or imgui
 * stuff.
 *
 * It is simply a collection of bucket renderers, and a few special case ones.
 */
class OpenGLRenderer {
 public:
  OpenGLRenderer(std::shared_ptr<TexturePool> texture_pool, std::shared_ptr<Loader> loader);

  // rendering interface: takes the dma chain from the game, and some size/debug settings from
  // the graphics system.
  void render(DmaFollower dma, const RenderOptions& settings);

 private:
  void setup_frame(const RenderOptions& settings);
  void dispatch_buckets(DmaFollower dma, ScopedProfilerNode& prof, bool sync_after_buckets);
  void do_pcrtc_effects(float alp, SharedRenderState* render_state, ScopedProfilerNode& prof);
  void init_bucket_renderers();
  void draw_renderer_selection_window();
  void finish_screenshot(const std::string& output_name, int px, int py, int x, int y, GLuint fbo);
  template <typename T, class... Args>
  T* init_bucket_renderer(const std::string& name,
                          BucketCategory cat,
                          BucketId id,
                          Args&&... args) {
    auto renderer = std::make_unique<T>(name, id, std::forward<Args>(args)...);
    T* ret = renderer.get();
    m_bucket_renderers.at((int)id) = std::move(renderer);
    m_bucket_categories.at((int)id) = cat;
    return ret;
  }

  SharedRenderState m_render_state;
  Profiler m_profiler;
  SmallProfiler m_small_profiler;
  SubtitleEditor m_subtitle_editor;

  std::array<std::unique_ptr<BucketRenderer>, (int)BucketId::MAX_BUCKETS> m_bucket_renderers;
  std::array<BucketCategory, (int)BucketId::MAX_BUCKETS> m_bucket_categories;

  std::array<float, (int)BucketCategory::MAX_CATEGORIES> m_category_times;
  FullScreenDraw m_blackout_renderer;
  CollideMeshRenderer m_collide_renderer;

  float m_last_pmode_alp = 1.;
  bool m_enable_fast_blackout_loads = true;

  struct FboState {
    struct {
      Fbo window;          // provided by glfw
      Fbo render_buffer;   // temporary buffer to render to
      Fbo resolve_buffer;  // temporary buffer to resolve to
    } resources;

    Fbo* render_fbo = nullptr;  // the selected fbo from the three above to use for rendering
  } m_fbo_state;
};
