text: #include "opengl45_gpu_device.hpp"
  #include "opengl45_gpu_texture.hpp"
  #include "opengl45_gpu_buffer.hpp"
  #include "opengl45_gpu_pipeline_state.hpp"
  #include "opengl45_gpu_draw_context.hpp"
  
  #include <glad/glad.h>
  #include <cassert>
  #include <optional>
  #include <sstream>
  #include <iostream>
  
  namespace bp
  {
  
  static GLenum opengl_internal_texture_format(gpu_texture_format format)
  {
      switch(format)
      {
          case gpu_texture_format::rgb8:
              return GL_RGB8;
          case gpu_texture_format::rgba8:
              return GL_RGBA8;
      }
  
      // Should never reach here
      assert(false);
  
      return 0;
  }
  
  std::unique_ptr<gpu_texture2d> opengl45_gpu_device::make_texture(const gpu_texture2d_description& desc, const void* pixels)
  {
      // Create a new OpenGL texture 2D
      GLuint new_texture;
      glCreateTextures(GL_TEXTURE_2D, 1, &new_texture);
  
      // Setup wrapping on new texture
      switch(desc.wrapping)
      {
          case gpu_texture_wrapping::clamp:
              glTextureParameteri(new_texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
              glTextureParameteri(new_texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
              break;
          case gpu_texture_wrapping::repeat:
              glTextureParameteri(new_texture, GL_TEXTURE_WRAP_S, GL_REPEAT);
              glTextureParameteri(new_texture, GL_TEXTURE_WRAP_T, GL_REPEAT);
              break;
          case gpu_texture_wrapping::mirror:
              glTextureParameteri(new_texture, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
              glTextureParameteri(new_texture, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
              break;
      }
  
      // Setup filtering on new texture
      switch(desc.filtering)
      {
          case gpu_texture_filtering::point:
              glTextureParameteri(new_texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
              glTextureParameteri(new_texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
              break;
          case gpu_texture_filtering::bilinear:
              glTextureParameteri(new_texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
              glTextureParameteri(new_texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
              break;
          case gpu_texture_filtering::trilinear:
              glTextureParameteri(new_texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
              glTextureParameteri(new_texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
              break;
      }
  
      // Allocate memory on GPU for the new texture
      glTextureStorage2D(new_texture,
                         // The number of texture levels
                         1,
                         // The pixel format of the texture
                         opengl_internal_texture_format(desc.format),
                         // The dimension of the texture
                         static_cast<GLsizei>(desc.width), static_cast<GLsizei>(desc.height));
  
      // TODO: Make pixel upload more configurable
      glTextureSubImage2D(new_texture, 0, 0, 0, desc.width, desc.height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
  
      if(desc.generate_mipmaps)
      {
          glGenerateTextureMipmap(new_texture);
      }
  
      return std::make_unique<opengl45_gpu_texture2d>(desc, new_texture);
  }
  
  std::unique_ptr<gpu_buffer> opengl45_gpu_device::make_buffer(const gpu_buffer_description& desc, const void* data)
  {
      GLuint new_buffer;
      glCreateBuffers(1, &new_buffer);
  
      // TODO: Make this more configurable
      // TODO: Add support for mutable storage
      glNamedBufferStorage(new_buffer, desc.size, data, 0);
  
      return std::make_unique<opengl45_gpu_buffer>(desc, new_buffer);
  }
  
  struct glsl_layout_attribute
  {
      gpu_vertex_attribute attribute;
  
      glsl_layout_attribute(gpu_vertex_attribute attribute) noexcept
      : attribute(attribute)
      {
  
      }
  };
  
  std::ostream& operator<<(std::ostream& os, const glsl_layout_attribute& glsl_attribute)
  {
      switch(glsl_attribute.attribute.type)
      {
          case gpu_vertex_attribute_type::float1:
              os << "float";
              break;
          case gpu_vertex_attribute_type::float2:
              os << "vec2";
              break;
          case gpu_vertex_attribute_type::float3:
              os << "vec3";
              break;
          case gpu_vertex_attribute_type::float4:
              os << "vec4";
              break;
          case gpu_vertex_attribute_type::int1:
              os << "int";
              break;
          case gpu_vertex_attribute_type::int2:
              os << "ivec2";
              break;
          case gpu_vertex_attribute_type::int3:
              os << "ivec3";
              break;
          case gpu_vertex_attribute_type::int4:
              os << "ivec4";
              break;
      }
  
      os << " " << glsl_attribute.attribute.name;
  
      return os;
  }
  
  static std::string generate_glsl_attribute_layouts(const gpu_vertex_layout& layout)
  {
      std::ostringstream stream;
  
      for(int i = 0; i < layout.attribute_count; ++i)
      {
          const gpu_vertex_attribute& attribute = layout.attributes[i];
  
          stream << "layout(location = " << i << ") in " << glsl_layout_attribute{attribute} << ";\n";
      }
  
      return stream.str();
  }
  
  static std::optional<GLuint> compile_shader(GLenum shader_type, std::string_view source, const gpu_pipeline_state_description& pipeline_state)
  {
      GLuint new_shader = glCreateShader(shader_type);
  
      static const char* source_header = "#version 450 core\n";
  
      const char* source_cstr = source.data();
  
      std::string attribute_layout = shader_type == GL_VERTEX_SHADER ? generate_glsl_attribute_layouts(pipeline_state.vertex_layout) : "";
  
      const char* source_sections[] = {
              source_header,
              attribute_layout.c_str(),
              source_cstr
      };
  
      glShaderSource(new_shader, sizeof(source_sections) / sizeof(const char*), source_sections, nullptr);
      glCompileShader(new_shader);
  
      GLint compilation_status = 0;
      glGetShaderiv(new_shader, GL_COMPILE_STATUS, &compilation_status);
  
      if(compilation_status == GL_FALSE)
      {
          char info_log_buffer[2048] = {0};
  
          GLsizei log_length = 0;
          glGetShaderInfoLog(new_shader, 2048, &log_length, info_log_buffer);
  
          std::cerr << info_log_buffer << std::endl;
  
          glDeleteShader(new_shader);
  
          return std::nullopt;
      }
  
      return new_shader;
  }
  
  static GLuint compile_shader(GLenum shader_type, const gpu_pipeline_stage_description& stage_description, const gpu_pipeline_state_description& pipeline_state)
  {
      // For now, only support glsl language
      assert(stage_description.language == gpu_shader_source_language::glsl);
  
      GLuint new_shader = 0;
  
      if(stage_description.enabled)
      {
          new_shader = *compile_shader(shader_type, stage_description.source, pipeline_state);
      }
  
      return new_shader;
  }
  
  static GLuint link_program(const gpu_pipeline_state_description& state_description)
  {
      GLuint new_program = glCreateProgram();
  
      const GLuint compiled_shaders[] = {
              compile_shader(GL_VERTEX_SHADER, state_description.vertex_stage, state_description),
              compile_shader(GL_FRAGMENT_SHADER, state_description.fragment_stage, state_description),
              compile_shader(GL_COMPUTE_SHADER, state_description.compute_stage, state_description)
      };
  
      for(GLuint compiled_shader : compiled_shaders)
      {
          if(compiled_shader != 0)
          {
              glAttachShader(new_program, compiled_shader);
          }
      }
  
      glLinkProgram(new_program);
  
      for(GLuint compiled_shader : compiled_shaders)
      {
          if(compiled_shader != 0)
          {
              glDetachShader(new_program, compiled_shader);
          }
      }
  
      GLint link_status;
      glGetProgramiv(new_program, GL_LINK_STATUS, &link_status);
  
      if(link_status == GL_FALSE)
      {
          // Release every compiled shaders
          for(GLuint compiled_shader : compiled_shaders)
          {
              if(compiled_shader != 0)
              {
                  glDeleteShader(compiled_shader);
              }
          }
  
          glDeleteProgram(new_program);
  
          return 0;
      }
  
      return new_program;
  }
  
  static GLint gl_attribute_count(gpu_vertex_attribute_type type)
  {
      switch(type)
      {
      case gpu_vertex_attribute_type::float1:
          return 1;
      case gpu_vertex_attribute_type::float2:
          return 2;
      case gpu_vertex_attribute_type::float3:
          return 3;
      case gpu_vertex_attribute_type::float4:
          return 4;
      case gpu_vertex_attribute_type::int1:
          return 1;
      case gpu_vertex_attribute_type::int2:
          return 2;
      case gpu_vertex_attribute_type::int3:
          return 3;
      case gpu_vertex_attribute_type::int4:
          return 4;
      default:
          return 0;
      }
  }
  
  static GLenum gl_attribute_type(gpu_vertex_attribute_type type)
  {
      switch(type)
      {
      case gpu_vertex_attribute_type::float1:
      case gpu_vertex_attribute_type::float2:
      case gpu_vertex_attribute_type::float3:
      case gpu_vertex_attribute_type::float4:
          return GL_FLOAT;
      case gpu_vertex_attribute_type::int1:
      case gpu_vertex_attribute_type::int2:
      case gpu_vertex_attribute_type::int3:
      case gpu_vertex_attribute_type::int4:
          return GL_INT;
      default:
          return 0;
      }
  }
  
  static GLuint gl_attribute_size(gpu_vertex_attribute_type type)
  {
      switch(type)
      {
      case gpu_vertex_attribute_type::float1:
          return sizeof(float) * 1;
      case gpu_vertex_attribute_type::float2:
          return sizeof(float) * 2;
      case gpu_vertex_attribute_type::float3:
          return sizeof(float) * 3;
      case gpu_vertex_attribute_type::float4:
          return sizeof(float) * 4;
      case gpu_vertex_attribute_type::int1:
          return sizeof(int) * 1;
      case gpu_vertex_attribute_type::int2:
          return sizeof(int) * 2;
      case gpu_vertex_attribute_type::int3:
          return sizeof(int) * 3;
      case gpu_vertex_attribute_type::int4:
          return sizeof(int) * 4;
      default:
          return 0;
      }
  }
  
  static GLuint make_vertex_array_from_layout(const gpu_vertex_layout& layout, GLuint& out_stride)
  {
      GLuint vao = 0;
      glCreateVertexArrays(1, &vao);
  
      GLuint offset = 0;
      for(GLuint i = 0; i < layout.attribute_count; ++i)
      {
          const gpu_vertex_attribute& attribute = layout.attributes[i];
  
          glEnableVertexArrayAttrib(vao, i);
          glVertexArrayAttribFormat(vao, i, gl_attribute_count(attribute.type), gl_attribute_type(attribute.type), attribute.is_normalized ? GL_TRUE : GL_FALSE, offset);
          glVertexArrayAttribBinding(vao, i, 0);
  
          offset += gl_attribute_size(attribute.type);
      }
  
      out_stride = offset;
  
      return vao;
  }
  
  std::unique_ptr<gpu_pipeline_state> opengl45_gpu_device::make_pipeline_state(const gpu_pipeline_state_description& desc)
  {
      // TODO: Try to reuse vertex arrays here
      GLuint layout_stride = 0;
      GLuint vao = make_vertex_array_from_layout(desc.vertex_layout, layout_stride);
  
      // Try to reuse programs here
      GLuint new_program = link_program(desc);
      if(new_program == 0)
      {
          return nullptr;
      }
  
      return std::make_unique<opengl45_gpu_pipeline_state>(new_program, vao, layout_stride, desc);
  }
  
  std::unique_ptr<gpu_draw_context> opengl45_gpu_device::make_draw_context()
  {
      return std::make_unique<opengl45_gpu_draw_context>();
  }
  
  }