/*
 *  <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/>.
 */
#include <blt/gfx/shader.h>
#include <blt/fs/loader.h>
#include <blt/std/string.h>
#include <blt/std/memory.h>

namespace blt::gfx
{
    uniform_buffer::uniform_buffer(size_t size, GLuint location): size_(size), location_(location)
    {
        glGenBuffers(1, &uboID);
        bind();
        glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(size), nullptr, GL_DYNAMIC_READ);
        unbind();
        
        glBindBufferBase(GL_UNIFORM_BUFFER, location, uboID);
    }
    
    uniform_buffer::uniform_buffer(void* data, size_t size, GLuint location): size_(size), location_(location)
    {
        glGenBuffers(1, &uboID);
        bind();
        glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(size), data, GL_DYNAMIC_READ);
        unbind();
        
        glBindBufferBase(GL_UNIFORM_BUFFER, location, uboID);
    }
    
    uniform_buffer& uniform_buffer::resize(size_t newSize)
    {
        bind();
        size_ = newSize;
        glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(size_), nullptr, GL_DYNAMIC_READ);
        unbind();
        return *this;
    }
    
    uniform_buffer& uniform_buffer::upload(void* data, size_t size, size_t offset)
    {
        bind();
        glBufferSubData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(offset), static_cast<GLsizeiptr>(size), data);
        unbind();
        return *this;
    }
    
    uniform_buffer& uniform_buffer::bind()
    {
        glBindBuffer(GL_UNIFORM_BUFFER, uboID);
        return *this;
    }
    
    uniform_buffer::~uniform_buffer()
    {
        glDeleteBuffers(1, &uboID);
    }
    
    static std::string removeEmptyFirstLines(std::string_view string)
    {
        auto lines = blt::string::split_sv(string, "\n");
        std::string new_source_string;
        for (const auto& line : lines)
        {
            if (!blt::string::trim(line).empty() && !blt::string::starts_with(line, "\""))
            {
                new_source_string += line;
                new_source_string += "\n";
            }
        }
        return new_source_string;
    }
    
    unsigned int shader_t::createShader(const std::string& source, int type)
    {
        const char* shader_code = source.c_str();
        // creates a Shader
        unsigned int shaderID = glCreateShader(type);
        // loads the shader code for later complication and uploading into the graphics card
        // TODO: defines can be added here by sending them as additional strings. No need to edit the source string
        glShaderSource(shaderID, 1, &shader_code, nullptr);
        
        // Compile it
        glCompileShader(shaderID);
        
        // make sure there are no errors in the compilation. If there is then print out information pertaining to the error.
        // the actual log is highly dependent on the platform this is being run from, so we cannot make any assumptions about the issue.
        // the TODO: maybe find a way of lexing the output to give suggestions about fixing the error? default error messages can be unhelpful at times.
        GLint success;
        glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            int log_length = 0;
            glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &log_length);
            
            // scoped buffers will delete their memory when they go out of scope. A benefit of using BLT
            blt::scoped_buffer<GLchar> infoLog{static_cast<unsigned long>(log_length + 1)};
            
            glGetShaderInfoLog(shaderID, log_length + 1, nullptr, infoLog.data());
            auto shader_type_str = (type == GL_VERTEX_SHADER ? "Vertex Shader" : type == GL_FRAGMENT_SHADER ? "Fragment Shader" : "Other Shader");
            BLT_ERROR("--- --- --- --- --- --- --- --- ---");
            BLT_ERROR("Unable to compile shader of type %s\nShader source:", shader_type_str);
            BLT_ERROR(source);
            BLT_ERROR("I have an log of %d length", log_length);
            BLT_ERROR(infoLog.data());
            BLT_ERROR("--- --- --- --- --- --- --- --- ---");
        }
        return shaderID;
    }
    
    shader_t::shader_t(std::string_view vertex, std::string_view fragment)
    {
        // load shader sources
        std::string vertex_source = removeEmptyFirstLines(vertex);
        std::string fragment_source = removeEmptyFirstLines(fragment);
        
        // create the shaders
        vertexShaderID = createShader(vertex_source, GL_VERTEX_SHADER);
        fragmentShaderID = createShader(fragment_source, GL_FRAGMENT_SHADER);
        
        // bind them to a program
        programID = glCreateProgram();
        // attach the loaded shaders to the Shader program
        glAttachShader(programID, vertexShaderID);
        glAttachShader(programID, fragmentShaderID);
        // link and make sure that our program is valid.
        glLinkProgram(programID);
        
        GLint success;
        glGetProgramiv(programID, GL_LINK_STATUS, &success);
        if (!success)
        {
            int log_length = 0;
            glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &log_length);
            
            // scoped buffers will delete their memory when they go out of scope.
            blt::scoped_buffer<GLchar> infoLog{static_cast<unsigned long>(log_length + 1)};
            
            glGetProgramInfoLog(programID, log_length + 1, nullptr, infoLog.data());
            BLT_ERROR("--- --- --- --- --- --- --- --- ---");
            BLT_ERROR("Unable to link program of ID: %d", programID);
            BLT_ERROR(vertex_source);
            BLT_ERROR(fragment_source);
            BLT_ERROR("I have an log of %d length", log_length);
            BLT_ERROR(infoLog.data());
            BLT_ERROR("--- --- --- --- --- --- --- --- ---");
            std::exit(0);
        }
        
        glValidateProgram(programID);
        bind();
        setUniformBlockLocation("GlobalMatrices", 0);
        glUseProgram(0);
    }
    
    void shader_t::bindAttribute(int attribute, const std::string& name) const
    {
        bind();
        glBindAttribLocation(programID, attribute, name.c_str());
    }
    
    void shader_t::setUniformBlockLocation(const std::string& name, int location) const
    {
        bind();
        auto blockID = glGetUniformBlockIndex(programID, name.c_str());
        if (blockID != GL_INVALID_INDEX)
            glUniformBlockBinding(programID, blockID, location);
        else
            BLT_WARN("Unable to find uniform buffer '%s'. Did you forget to declare it?", name.c_str());
    }
    
    shader_t::~shader_t()
    {
        glUseProgram(0);
        // shader was moved
        if (programID <= 0)
            return;
        // remove all the shaders from the program
        glDetachShader(programID, vertexShaderID);
        glDetachShader(programID, fragmentShaderID);
        
        // delete the shaders
        glDeleteShader(vertexShaderID);
        glDeleteShader(fragmentShaderID);
        
        // delete the Shader program
        glDeleteProgram(programID);
    }
    
    shader_t::shader_t(shader_t&& move) noexcept
    {
        // the move constructor doesn't need to construct a new shader but it does need to ensure all old variables are moved over
        programID = move.programID;
        vertexShaderID = move.vertexShaderID;
        fragmentShaderID = move.fragmentShaderID;
        for (const auto& pair : move.uniformVars)
            uniformVars.insert(pair);
        // by setting the program ID to -1 we tell the shader it has been moved.
        move.programID = -1;
    }
    
    std::string shader_t::loadShader(std::string_view file)
    {
        auto lines = blt::fs::recursiveInclude(file);
        std::string str;
        for (const auto& v : lines)
            str += v;
        return str;
    }
    
    shader_t* shader_t::make(std::optional<std::reference_wrapper<template_engine_t>> engine, std::string_view vertex,
                             std::string_view fragment)
    {
        std::string vertex_source;
        std::string fragment_source;
        if (engine)
        {
            auto s_pair = process_templates(engine->get(), vertex, fragment);
            vertex_source = std::move(s_pair.first);
            fragment_source = std::move(s_pair.second);
        } else
        {
            vertex_source = vertex;
            fragment_source = fragment;
        }
        
        return new shader_t(vertex_source, fragment_source);
    }
    
    shader_t* shader_t::load(std::optional<std::reference_wrapper<template_engine_t>> engine, std::string_view vertex,
                             std::string_view fragment)
    {
        auto vertex_source = loadShader(vertex);
        auto fragment_source = loadShader((fragment));
        return make(engine, vertex_source, fragment_source);
    }
    
    std::pair<std::string, std::string> shader_t::process_templates(template_engine_t& engine, std::string_view vertex, std::string_view fragment)
    {
        auto vertex_e = engine.evaluate(vertex);
        auto fragment_e = engine.evaluate(fragment);
        if (!vertex_e)
        {
            BLT_ERROR("Error vertex function returned: %d", static_cast<int>(vertex_e.error()));
            throw std::runtime_error("Unable to substitute vertex source");
        }
        if (!fragment_e)
        {
            BLT_ERROR("Error fragment function returned: %d", static_cast<int>(vertex_e.error()));
            throw std::runtime_error("Unable to substitute fragment source");
        }
        return {vertex_e.value(), fragment_e.value()};
    }
    
    std::unique_ptr<shader_t> shader_t::make_unique(std::optional<std::reference_wrapper<template_engine_t>> engine, std::string_view vertex,
                                                    std::string_view fragment)
    {
        return std::unique_ptr<shader_t>(shader_t::make(engine, vertex, fragment));
    }
    
    std::unique_ptr<shader_t> shader_t::load_unique(std::optional<std::reference_wrapper<template_engine_t>> engine, std::string_view vertex,
                                                    std::string_view fragment)
    {
        auto vertex_source = loadShader(vertex);
        auto fragment_source = loadShader((fragment));
        return make_unique(engine, vertex_source, fragment_source);
    }
    
    shader_base_t& shader_base_t::setBool(const std::string& name, bool value)
    {
        if (auto i = getUniformLocation(name))
            glUniform1i(i.i, (int) value);
        else
            BLT_WARN("Unable to find boolean uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setInt(const std::string& name, int value)
    {
        if (auto i = getUniformLocation(name))
            glUniform1i(i.i, value);
        else
            BLT_WARN("Unable to find integer uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setFloat(const std::string& name, float value)
    {
        if (auto i = getUniformLocation(name))
            glUniform1f(i.i, value);
        else
            BLT_WARN("Unable to find float uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setMatrix(const std::string& name, mat4x4& matrix)
    {
        if (auto i = getUniformLocation(name))
            glUniformMatrix4fv(i.i, 1, GL_FALSE, matrix.ptr());
        else
            BLT_WARN("Unable to find matrix uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec2(const std::string& name, const vec2& vec)
    {
        if (auto i = getUniformLocation(name))
            glUniform2f(i.i, vec.x(), vec.y());
        else
            BLT_WARN("Unable to find vec3 uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec3(const std::string& name, const vec3& vec)
    {
        if (auto i = getUniformLocation(name))
            glUniform3f(i.i, vec.x(), vec.y(), vec.z());
        else
            BLT_WARN("Unable to find vec3 uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec4(const std::string& name, const vec4& vec)
    {
        if (auto i = getUniformLocation(name))
            glUniform4f(i.i, vec.x(), vec.y(), vec.z(), vec.w());
        else
            BLT_WARN("Unable to find vec4 uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec2(const std::string& name, float x, float y)
    {
        if (auto i = getUniformLocation(name))
            glUniform2f(i.i, x, y);
        else
            BLT_WARN("Unable to find vec2 uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec3(const std::string& name, float x, float y, float z)
    {
        if (auto i = getUniformLocation(name))
            glUniform3f(i.i, x, y, z);
        else
            BLT_WARN("Unable to find vec3 uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec4(const std::string& name, float x, float y, float z, float w)
    {
        if (auto i = getUniformLocation(name))
            glUniform4f(i.i, x, y, z, w);
        else
            BLT_WARN("Unable to find vec4 uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t& shader_base_t::setVec4i(const std::string& name, const vec4i& vec)
    {
        if (auto i = getUniformLocation(name))
            glUniform4i(i.i, vec.x(), vec.y(), vec.z(), vec.w());
        else
            BLT_WARN("Unable to find vec4i uniform variable %s", name.c_str());
        return *this;
    }
    
    shader_base_t::IntDefaultedToMinusOne shader_base_t::getUniformLocation(const std::string& name)
    {
        if (uniformVars[name])
            return uniformVars[name];
        int loc = glGetUniformLocation(programID, name.c_str());
        auto& v = uniformVars[name];
        v.i = loc;
        return v;
    }
    
    shader_base_t& shader_base_t::setMatrix(const std::string& name, mat4x4&& matrix)
    {
        mat4x4 mat = matrix;
        return setMatrix(name, mat);
    }
}
