/*
 *  <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_SHADER_H
#define BLT_WITH_GRAPHICS_SHADER_H

#include <blt/gfx/gl_includes.h>
#include <unordered_map>
#include <string>
#include <blt/math/math.h>

namespace blt::gfx
{
    class uniform_buffer
    {
        private:
            GLuint uboID = 0;
            size_t size_;
            GLuint location_;

        public:
            explicit uniform_buffer(size_t size, GLuint location = 0);

            uniform_buffer(void* data, size_t size, GLuint location = 0);

            /**
             * Resizes the internal UBO
             * @param newSize new size for the UBO
             */
            uniform_buffer& resize(size_t newSize);

            /**
             * Uploads data to the UBO. This can be an arbitrary locations and does not need to be the whole UBO.
             */
            uniform_buffer& upload(void* data, size_t size, size_t offset = 0);

            uniform_buffer& bind();

            inline uniform_buffer& unbind()
            {
                glBindBuffer(GL_UNIFORM_BUFFER, 0);
                return *this;
            }

            ~uniform_buffer();

            [[nodiscard]] inline size_t size() const { return size_; }

            [[nodiscard]] inline GLuint location() const { return location_; }
    };

    class shader_base_t
    {
        friend uniform_buffer;

        protected:
            struct IntDefaultedToMinusOne
            {
                GLint i = -1;

                inline explicit operator bool() const { return i != -1; }
            };

            std::unordered_map<std::string, IntDefaultedToMinusOne> uniformVars;
            GLuint programID = 0;

            IntDefaultedToMinusOne getUniformLocation(const std::string& name);

        public:
            const shader_base_t& bind() const
            {
                glUseProgram(programID);
                return *this;
            }

            shader_base_t& bind()
            {
                glUseProgram(programID);
                return *this;
            }

            shader_base_t& unbind()
            {
                glUseProgram(0);
                return *this;
            }

            shader_base_t& setBool(const std::string& name, bool value);

            shader_base_t& setInt(const std::string& name, int value);

            shader_base_t& setFloat(const std::string& name, float value);

            shader_base_t& setMatrix(const std::string& name, blt::mat4x4& matrix);

            // poor solution: TODO
            shader_base_t& setMatrix(const std::string& name, blt::mat4x4&& matrix);

            shader_base_t& setVec2(const std::string& name, const blt::vec2& vec);

            shader_base_t& setVec3(const std::string& name, const blt::vec3& vec);

            shader_base_t& setVec4(const std::string& name, const blt::vec4& vec);

            shader_base_t& setVec2(const std::string& name, float x, float y);

            shader_base_t& setVec3(const std::string& name, float x, float y, float z);

            shader_base_t& setVec4(const std::string& name, float x, float y, float z, float w);
    };

    /**
     * a basic computer shader class, contains the functions and resources required to use compute shaders!
     */
    class compute_shader_t : public shader_base_t
    {
        private:
            GLuint shaderID = 0;

        public:
            explicit compute_shader_t(const std::string& shader_source, bool loadAsString = true);

            void execute(const int x, const int y, const int z) const
            {
                bind();
                glDispatchCompute(x, y, z);
            }

            ~compute_shader_t();
    };

    class shader_t : public shader_base_t
    {
        private:
            GLuint vertexShaderID = 0;
            GLuint fragmentShaderID = 0;

            static unsigned int createShader(const std::string& source, int type);

            static std::string loadShader(std::string_view file);

        public:
            /**
             * Creates a shader
             * @param vertex vertex shader source or file
             * @param fragment fragment shader source or file
             * @param geometry geometry shader source or file (optional)
             * @param load_as_string load the shader as a string (true) or use the string to load the shader as a file (false)
             */
            shader_t(const std::string& vertex, const std::string& fragment, bool load_as_string = true);

            shader_t(shader_t&& move) noexcept;

            // used to set the location of VAOs to the in variables in opengl shaders.
            void bindAttribute(int attribute, const std::string& name) const;

            // used to set location of shared UBOs like the perspective and view matrix
            void setUniformBlockLocation(const std::string& name, int location) const;

            ~shader_t();
    };
}

#endif //BLT_WITH_GRAPHICS_SHADER_H