#ifndef ENGINE_RENDERER_OPENGL_RENDERER_OPENGL_H
#define ENGINE_RENDERER_OPENGL_RENDERER_OPENGL_H

#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

#define THREADED_RENDERING

#ifdef THREADED_RENDERING
#include <condition_variable>
#include <deque>
#include <future>
#include <mutex>
#include <semaphore>
#include <thread>
#endif  // THREADED_RENDERING

#include "engine/renderer/opengl/opengl.h"

#include "engine/renderer/renderer.h"

#if defined(__ANDROID__)
struct ANativeWindow;
#endif

#ifdef THREADED_RENDERING
namespace base {
class TaskRunner;
}
#endif  // THREADED_RENDERING

namespace eng {

struct RenderCommand;

class RendererOpenGL : public Renderer {
 public:
  RendererOpenGL();
  ~RendererOpenGL() override;

#if defined(__ANDROID__)
  bool Initialize(ANativeWindow* window) override;
#elif defined(__linux__)
  bool Initialize(Display* display, Window window) override;
#endif

  void Shutdown() override;

  uint64_t CreateGeometry(std::unique_ptr<Mesh> mesh) override;
  void DestroyGeometry(uint64_t resource_id) override;
  void Draw(uint64_t resource_id) override;

  uint64_t CreateTexture() override;
  void UpdateTexture(uint64_t resource_id,
                     std::unique_ptr<Image> image) override;
  void DestroyTexture(uint64_t resource_id) override;
  void ActivateTexture(uint64_t resource_id) override;

  uint64_t CreateShader(std::unique_ptr<ShaderSource> source,
                        const VertexDescripton& vertex_description,
                        Primitive primitive,
                        bool enable_depth_test) override;
  void DestroyShader(uint64_t resource_id) override;
  void ActivateShader(uint64_t resource_id) override;

  void SetUniform(uint64_t resource_id,
                  const std::string& name,
                  const base::Vector2f& val) override;
  void SetUniform(uint64_t resource_id,
                  const std::string& name,
                  const base::Vector3f& val) override;
  void SetUniform(uint64_t resource_id,
                  const std::string& name,
                  const base::Vector4f& val) override;
  void SetUniform(uint64_t resource_id,
                  const std::string& name,
                  const base::Matrix4f& val) override;
  void SetUniform(uint64_t resource_id,
                  const std::string& name,
                  float val) override;
  void SetUniform(uint64_t resource_id,
                  const std::string& name,
                  int val) override;
  void UploadUniforms(uint64_t resource_id) override {}

  void PrepareForDrawing() override {}
  void Present() override;

  size_t GetAndResetFPS() override;

#if defined(__linux__) && !defined(__ANDROID__)
  XVisualInfo* GetXVisualInfo(Display* display) override;
#endif

 private:
  struct GeometryOpenGL {
    struct Element {
      GLsizei num_elements;
      GLenum type;
      size_t vertex_offset;
    };

    GLsizei num_vertices = 0;
    GLsizei num_indices = 0;
    GLenum primitive = 0;
    GLenum index_type = 0;
    std::vector<Element> vertex_layout;
    GLuint vertex_size = 0;
    GLuint vertex_array_id = 0;
    GLuint vertex_buffer_id = 0;
    GLuint index_buffer_id = 0;
  };

  struct ShaderOpenGL {
    GLuint id = 0;
    std::unordered_map<std::string, GLuint> uniforms;
    bool enable_depth_test = false;
  };

  struct TextureOpenGL {
    GLuint id = 0;
  };

  std::unordered_map<uint64_t, GeometryOpenGL> geometries_;
  std::unordered_map<uint64_t, ShaderOpenGL> shaders_;
  std::unordered_map<uint64_t, TextureOpenGL> textures_;
  uint64_t last_resource_id_ = 0;

  GLuint active_shader_id_ = 0;
  GLuint active_texture_id_ = 0;

  bool vertex_array_objects_ = false;
  bool npot_ = false;

#ifdef THREADED_RENDERING
  // Global commands are independent from frames and guaranteed to be processed.
  std::deque<std::unique_ptr<RenderCommand>> global_commands_;
  // Draw commands are fame specific and can be discarded if the rendering is
  // not throttled.
  std::deque<std::unique_ptr<RenderCommand>> draw_commands_[2];

  std::condition_variable cv_;
  std::mutex mutex_;
  std::thread render_thread_;
  bool terminate_render_thread_ = false;

  std::counting_semaphore<> draw_complete_semaphore_{0};

  base::TaskRunner* main_thread_task_runner_;
#endif  // THREADED_RENDERING

  // Stats.
  size_t fps_ = 0;

#if defined(__ANDROID__)
  ANativeWindow* window_;
#elif defined(__linux__)
  Display* display_ = NULL;
  Window window_ = 0;
  GLXContext glx_context_ = NULL;
#endif

  bool InitInternal();
  bool InitCommon();
  void ShutdownInternal();

  void ContextLost();

  void DestroyAllResources();

  bool StartRenderThread();
  void TerminateRenderThread();

#ifdef THREADED_RENDERING
  void RenderThreadMain(std::promise<bool> promise);
#endif  // THREADED_RENDERING

  void EnqueueCommand(std::unique_ptr<RenderCommand> cmd);
  void ProcessCommand(RenderCommand* cmd);

  void HandleCmdPresent(RenderCommand* cmd);
  void HandleCmdCreateTexture(RenderCommand* cmd);
  void HandleCmdUpdateTexture(RenderCommand* cmd);
  void HandleCmdDestoryTexture(RenderCommand* cmd);
  void HandleCmdActivateTexture(RenderCommand* cmd);
  void HandleCmdCreateGeometry(RenderCommand* cmd);
  void HandleCmdDestroyGeometry(RenderCommand* cmd);
  void HandleCmdDrawGeometry(RenderCommand* cmd);
  void HandleCmdCreateShader(RenderCommand* cmd);
  void HandleCmdDestroyShader(RenderCommand* cmd);
  void HandleCmdActivateShader(RenderCommand* cmd);
  void HandleCmdSetUniformVec2(RenderCommand* cmd);
  void HandleCmdSetUniformVec3(RenderCommand* cmd);
  void HandleCmdSetUniformVec4(RenderCommand* cmd);
  void HandleCmdSetUniformMat4(RenderCommand* cmd);
  void HandleCmdSetUniformFloat(RenderCommand* cmd);
  void HandleCmdSetUniformInt(RenderCommand* cmd);

  void BindTexture(GLuint id);
  bool SetupVertexLayout(const VertexDescripton& vd,
                         GLuint vertex_size,
                         bool use_vao,
                         std::vector<GeometryOpenGL::Element>& vertex_layout);
  GLuint CreateShader(const char* source, GLenum type);
  bool BindAttributeLocation(GLuint id, const VertexDescripton& vd);
  GLint GetUniformLocation(GLuint id,
                           const std::string& name,
                           std::unordered_map<std::string, GLuint>& uniforms);
};

}  // namespace eng

#endif  // ENGINE_RENDERER_OPENGL_RENDERER_OPENGL_H
