#pragma once

#include "components/camera.hpp"
#include "components/lights.hpp"
#include "graphics/opengl/buffer.hpp"
#include "graphics/opengl/framebuffer.hpp"
#include "graphics/opengl/program.hpp"
#include "graphics/opengl/vertex_array.hpp"
#include "maths/transform.hpp"
#include "resources/material.hpp"
#include "resources/model.hpp"
#include "resources/primitive.hpp"

#include <glad/gl.h>

#include <array>
#include <glm/ext/matrix_transform.hpp>
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>

#include <cstddef>
#include <cstdint>
#include <limits>
#include <utility>
#include <vector>

namespace kuiper
{

namespace gl
{
    static constexpr std::size_t   MAX_NUM_LIGHTS       = 100;
    static constexpr std::size_t   LIGHT_SSBO_INIT_SIZE = 1000;
    static constexpr std::uint32_t NUM_CLUSTERS_X       = 12;
    static constexpr std::uint32_t NUM_CLUSTERS_Y       = 12;
    static constexpr std::uint32_t NUM_CLUSTERS_Z       = 24;
    static constexpr std::uint32_t NUM_CLUSTERS         = NUM_CLUSTERS_X * NUM_CLUSTERS_Y * NUM_CLUSTERS_Z;
    static constexpr std::uint32_t LOCAL_SIZE           = 128;
    static constexpr float         NEAR_Z_DEPTH         = 0.1f;
    static constexpr float         FAR_Z_DEPTH          = std::numeric_limits<std::uint16_t>::max();

    struct gl_state {
        // Vertex & element buffers
        vertex_array::s_ptr vao {nullptr};
        buffer::s_ptr       vertex_buffer {nullptr};
        buffer::s_ptr       index_buffer {nullptr};

        // Indirect & model matrix buffer

        buffer::s_ptr indirect_buffer {nullptr};
        buffer::s_ptr matrix_buffer {nullptr};

        // Shaders
        program::s_ptr pbr_shader {nullptr};
        program::s_ptr clustering_shader {nullptr};
        program::s_ptr light_culling_shader {nullptr};

        // Lighting buffers
        buffer::s_ptr cluster_buffer {nullptr};
        buffer::s_ptr lights_buffer {nullptr};

        // Configuration
        std::uint32_t             max_texture_units {32};
        std::vector<std::int32_t> texture_unit_slots {};
    };

    struct renderer_metrics {
        std::size_t n_vertices {};
        std::size_t n_indices {};
        std::size_t n_draw_cmds {};
        std::size_t n_matrices {};
        std::size_t n_materials {};
        std::size_t n_lights {};
    };

    class renderer {
      public:
        using vertex_t = vertex;
        using index_t  = std::uint32_t;

        struct draw_elements_indirect_cmd {
            std::uint32_t count {0};
            std::uint32_t instance_count {0};
            std::uint32_t first_index {0};
            std::uint32_t base_vertex {0};
            std::uint32_t base_instance {0};
        };

        struct alignas(4) render_matrices {
            glm::mat4 model_matrix;
            glm::mat3 normal_matrix;
        };

        struct alignas(16) cluster {
            glm::vec4     min_point;
            glm::vec4     max_point;
            std::uint32_t count;
            std::uint32_t light_indices[MAX_NUM_LIGHTS];
        };

        struct alignas(16) point_light {
            glm::vec4 position;
            glm::vec4 colour;
            float     intensity;
            float     radius;
        };

      private:
        using vertex_storage_t   = std::vector<vertex_t>;
        using index_storage_t    = std::vector<index_t>;
        using draw_cmd_storage_t = std::vector<draw_elements_indirect_cmd>;
        using matrix_storage_t   = std::vector<render_matrices>;
        using material_storage_t = std::vector<resource::material>;
        using light_storage_t    = std::vector<point_light>;

      public:
        renderer(gl_state&& state)
            : m_state(std::move(state)) {}
        ~renderer() = default;

        renderer(const renderer&)                = delete;
        renderer& operator=(const renderer&)     = delete;
        renderer(renderer&&) noexcept            = delete;
        renderer& operator=(renderer&&) noexcept = delete;

        static renderer init();

        void             render(const framebuffer::s_ptr&      framebuffer,
                                const glm::vec3&               cam_pos,
                                const component::camera_props& cam_props) noexcept;
        void             flush() noexcept;
        renderer_metrics metrics() const noexcept;

        void draw_model(const resource::model& model, const maths::transform_euler& transform);
        void draw_mesh(const resource::mesh&                  mesh,
                       const std::vector<resource::material>& materials,
                       const glm::mat4&                       transform);
        void draw_primitive(const resource::primitive&    primitive,
                            const resource::material&     material,
                            const maths::transform_euler& transform);
        void draw_primitive(const resource::primitive& primitive,
                            const resource::material&  material,
                            const glm::mat4&           transform);
        void add_light(const glm::vec3& pos, const component::point_light& light) noexcept;

      private:
        void update_and_upload() noexcept;

      private:
        gl_state m_state {};

        vertex_storage_t   m_vertices {};
        index_storage_t    m_indices {};
        draw_cmd_storage_t m_draw_cmds {};
        matrix_storage_t   m_matrices {};
        material_storage_t m_materials {};
        light_storage_t    m_lights {};
    };

} // namespace gl

} // namespace kuiper
