/*
 *  <Short Description>
 *  Copyright (C) 2023  Brett Terpstra
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifndef BLT_WITH_GRAPHICS_MODEL_H
#define BLT_WITH_GRAPHICS_MODEL_H

#include <blt/math/vectors.h>
#include <vector>
#include <blt/gfx/gl_includes.h>
#include <variant>
#include <array>
#include <blt/std/hashmap.h>

namespace blt::gfx
{
    struct vbo_t_owner;
    
    class static_dynamic_array;
    
    class vertex_array_t;
    
    // vbo
    struct vertex_buffer_t
    {
            friend vbo_t_owner;
            friend vertex_array_t;
            friend static_dynamic_array;
        private:
            GLuint bufferID_ = 0;
            GLsizeiptr size_ = 0;
            GLint buffer_type = 0;
            GLint memory_type = 0;
        public:
            
            void create(GLint type = GL_ARRAY_BUFFER);
            
            void bind() const;
            
            void unbind() const;
            
            void allocate(GLsizeiptr size, GLint mem_type = GL_STATIC_DRAW, const void* data = nullptr);
            
            template<typename T>
            void allocate(GLsizeiptr size, const T* data, GLint mem_type = GL_STATIC_DRAW)
            {
                allocate(size, mem_type, static_cast<const void*>(data));
            }
            
            void sub_update(GLsizeiptr offset, GLsizeiptr size, const void* data) const;
            
            void update(GLsizeiptr size, const void* data);
            
            void destroy();
    };
    
    // ssbo
    struct shader_buffer_t : public vertex_buffer_t
    {
        public:
            inline void create()
            {
                vertex_buffer_t::create(GL_SHADER_STORAGE_BUFFER);
            }
    };
    
    // ebo
    struct element_buffer_t : public vertex_buffer_t
    {
        public:
            inline void create()
            {
                vertex_buffer_t::create(GL_ELEMENT_ARRAY_BUFFER);
            }
    };
    
    struct vbo_t_owner
    {
        vertex_buffer_t vbo;
        
        vbo_t_owner() = default;
        
        explicit vbo_t_owner(vertex_buffer_t vbo): vbo(vbo)
        {}
        
        vertex_buffer_t* operator->()
        {
            return &vbo;
        }
        
        ~vbo_t_owner()
        {
            if (!vbo.bufferID_)
                return;
            vbo.destroy();
            vbo.unbind();
        }
    };
    
    /**
     * Since most VAOs will not use more than 8 VBOs it makes no sense to heap allocate memory to store them
     * This class is used to make that easier to handle
     */
    class static_dynamic_array
    {
        public:
            using vbo_type = std::shared_ptr<vbo_t_owner>;
        private:
            static constexpr size_t DATA_SIZE = 8;
            using array_t = std::array<vbo_type, DATA_SIZE>;
            std::variant<array_t, vbo_type*> data_;
            size_t size_ = DATA_SIZE;
            size_t max = 0;
            
            void swap();
        
        public:
            static_dynamic_array();
            
            static_dynamic_array(const static_dynamic_array& copy) = delete;
            
            static_dynamic_array(static_dynamic_array&& move) noexcept = default;
            
            static_dynamic_array& operator=(const static_dynamic_array& copy) = delete;
            
            static_dynamic_array& operator=(static_dynamic_array&& move) noexcept = default;
            
            vbo_type& operator[](size_t index);
            
            [[nodiscard]] inline size_t used() const noexcept
            {
                return max;
            }
            
            ~static_dynamic_array()
            {
                if (std::holds_alternative<vbo_type*>(data_))
                    delete[] std::get<vbo_type*>(data_);
            }
    };
    
    /**
     * basic VAO class.
     */
    class vertex_array_t
    {
        private:
            GLuint vaoID;
            static_dynamic_array VBOs;
            blt::hashset_t<GLuint> used_attributes;
            vertex_buffer_t element;
            
            void handle_vbo(const vertex_buffer_t& vbo, int attribute_number, int coordinate_size, GLenum type, int stride, long offset);
        
        public:
            vertex_array_t();
            
            vertex_array_t(const vertex_array_t&) = delete;
            
            vertex_array_t(vertex_array_t&&) = delete;
            
            vertex_array_t& operator=(const vertex_array_t&) = delete;
            
            vertex_array_t& operator=(vertex_array_t&&) = delete;
            
            /**
             * This function takes ownership of the underlying VBO (GPU side). It will be freed when the basic vertex array is deleted
             * @param vbo vbo to bind to this attribute
             * @param attribute_number attribute number to bind to
             * @param coordinate_size size of the data (number of
             * @param type type of data
             * @param stride how many bytes this data takes (for the entire per-vertex data structure) 0 will assume packed data
             *               This is in effect how many bytes until the next block of data
             * @param offset offset into the data structure to where the data is stored
             * @return a shared pointer to the stored vbo. used for chaining VAOs with multiple shared VBOs
             */
            void bindVBO(const vertex_buffer_t& vbo, int attribute_number, int coordinate_size, GLenum type, int stride, long offset);
            
            // same as the other bind method except you provide the shared reference.
            void bindVBO(const static_dynamic_array::vbo_type& vbo, int attribute_number, int coordinate_size, GLenum type, int stride, long offset);
            
            inline void bindElement(const vertex_buffer_t& vbo)
            {
                bind();
                element = vbo;
                vbo.bind();
                unbind();
            }
            
            inline vertex_buffer_t& getElement()
            {
                return element;
            }
            
            /**
             * Returns a non-owning reference to a vbo allowing for updating the VBO
             * The VBO is considered invalid if its ID is 0
             */
            inline vertex_buffer_t& operator[](size_t index)
            {
                return getBuffer(index);
            }
            
            inline vertex_buffer_t& getBuffer(size_t index)
            {
                return VBOs[index]->vbo;
            }
            
            inline void bind() const
            {
                glBindVertexArray(vaoID);
            }
            
            static inline void unbind()
            {
                glBindVertexArray(0);
            }
            
            static inline static_dynamic_array::vbo_type make_vbo(const vertex_buffer_t& vbo)
            {
                return std::make_shared<vbo_t_owner>(vbo);
            }
            
            ~vertex_array_t();
    };
    
}

#endif //BLT_WITH_GRAPHICS_MODEL_H