/*
 *  <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/renderer/batch_2d_renderer.h>
#include <blt/gfx/renderer/2d_textured.vert>
#include <blt/gfx/renderer/2d_line.vert>
#include <blt/gfx/renderer/2d_textured.frag>
#include <blt/gfx/renderer/2d_textured_circle.frag>
#include <blt/gfx/renderer/2d_line.frag>
// https://stackoverflow.com/questions/60440682/drawing-a-line-in-modern-opengl

float square_vertices[20] = {
        // positions        // texture coords
        0.5f, 0.5f, 0.0f, 1.0f, 1.0f,   // top right
        0.5f, -0.5f, 0.0f, 1.0f, 0.0f,   // bottom right
        -0.5f, -0.5f, 0.0f, 0.0f, 0.0f,   // bottom left
        -0.5f, 0.5f, 0.0f, 0.0f, 1.0f    // top left
};
const unsigned int square_indices[6] = {
        0, 1, 3,   // first triangle
        1, 2, 3    // second triangle
};

float line_vertices[20] = {
        // positions        // texture coords
        0.5f, 0.5f, 0.0f, 1.0f, 1.0f,   // top right
        0.5f, -0.5f, 0.0f, 1.0f, 0.0f,   // bottom right
        -0.5f, -0.5f, 0.0f, 0.0f, 0.0f,   // bottom left
        -0.5f, 0.5f, 0.0f, 0.0f, 1.0f    // top left
};

// 0, 1 (top right)
// 5, 6 (bottom right)
// 10, 11 (bottom left)
// 15, 16 (top left)

namespace blt::gfx
{
    
    void batch_renderer_2d::create()
    {
        {
            vbo_t vertices_vbo;
            ebo_t indices_vbo;
            
            vertices_vbo.create();
            indices_vbo.create();
            
            vertices_vbo.allocate(sizeof(square_vertices), square_vertices);
            indices_vbo.allocate(sizeof(square_indices), square_indices);
            
            square_vao = new vertex_array();
            auto tb = square_vao->bindVBO(vertices_vbo, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
            square_vao->bindVBO(tb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
            square_vao->bindElement(indices_vbo);
        }
        {
            vbo_t vertices_vbo;
            ebo_t indices_vbo;
            
            vertices_vbo.create();
            indices_vbo.create();
            
            vertices_vbo.allocate(sizeof(line_vertices), line_vertices);
            indices_vbo.allocate(sizeof(square_indices), square_indices);
            
            line_vao = new vertex_array();
            auto tb = line_vao->bindVBO(vertices_vbo, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
            line_vao->bindVBO(tb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
            line_vao->bindElement(indices_vbo);
        }
        
        square_shader = new shader_t(shader_2d_textured_vert, shader_2d_textured_frag);
        square_shader->bindAttribute(0, "vertex");
        square_shader->bindAttribute(1, "uv_in");
        
        point_shader = new shader_t(shader_2d_textured_vert, shader_2d_textured_cirlce_frag);
        point_shader->bindAttribute(0, "vertex");
        point_shader->bindAttribute(1, "uv_in");
    }
    
    void batch_renderer_2d::cleanup()
    {
        delete square_vao;
        delete square_shader;
        delete point_shader;
    }
    
    template<typename T>
    void find_min_and_max(T& container, blt::f32& min, blt::f32& max)
    {
        for (auto& textures : container)
        {
            for (auto& colors : textures.second)
            {
                for (auto& blend_factors : colors.second)
                {
                    for (auto& obj : blend_factors.second)
                    {
                        min = std::min(min, obj.z_index);
                        max = std::max(max, obj.z_index);
                    }
                }
            }
        }
    }
    
    void batch_renderer_2d::render(bool transparency)
    {
        if (transparency)
        {
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        }
        glEnable(GL_DEPTH_TEST);
        draw_count_ = 0;
        square_shader->bind();
        square_vao->bind();
        glActiveTexture(GL_TEXTURE0);
        
        // annoying
        blt::f32 min = std::numeric_limits<blt::f32>::max();
        blt::f32 max = std::numeric_limits<blt::f32>::min();
        
        find_min_and_max(complex_rectangles, min, max);
        find_min_and_max(complex_points, min, max);
        find_min_and_max(complex_lines, min, max);
        
        blt::f32 denominator = 1.0f / (max - min);
        
        for (auto& textures : complex_rectangles)
        {
            // resource manager handles the check for empty string
            if (auto val = resources.get(textures.first))
                val.value()->bind();
            for (auto& colors : textures.second)
            {
                square_shader->setVec4("color", colors.first);
                for (auto& blend_factors : colors.second)
                {
                    square_shader->setVec4("use_texture", blend_factors.first);
                    for (auto& rect_obj : blend_factors.second)
                    {
                        auto& rect = rect_obj.obj;
                        blt::mat4x4 model;
                        model.translate(rect.pos.x(), rect.pos.y(), 0.0f);
                        model.scale(rect.size.x(), rect.size.y(), 1);
                        if (rect.rotation != 0)
                            model.rotateZ(blt::toRadians(rect.rotation));
                        square_shader->setMatrix("model", model);
                        square_shader->setFloat("z_index", (rect_obj.z_index - min) * denominator);
                        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
                        draw_count_++;
                    }
                    blend_factors.second.clear();
                }
                colors.second.clear();
            }
            textures.second.clear();
        }
        
        point_shader->bind();
        for (auto& textures : complex_points)
        {
            // resource manager handles the check for empty string
            if (auto val = resources.get(textures.first))
                val.value()->bind();
            for (auto& colors : textures.second)
            {
                point_shader->setVec4("color", colors.first);
                for (auto& blend_factors : colors.second)
                {
                    point_shader->setVec4("use_texture", blend_factors.first);
                    for (auto& point_obj : blend_factors.second)
                    {
                        auto& point = point_obj.obj;
                        blt::mat4x4 model;
                        model.translate(point.pos.x(), point.pos.y(), 0.0f);
                        model.scale(point.scale, point.scale, 1);
                        point_shader->setMatrix("model", model);
                        point_shader->setFloat("z_index", (point_obj.z_index - min) * denominator);
                        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
                        draw_count_++;
                    }
                    blend_factors.second.clear();
                }
                colors.second.clear();
            }
            textures.second.clear();
        }
        
        blt::mat4x4 model;
        square_shader->bind();
        square_shader->setMatrix("model", model);
        line_vao->bind();
        auto& buf = line_vao->getBuffer(0);
        buf.bind();
        for (auto& textures : complex_lines)
        {
            // resource manager handles the check for empty string
            if (auto val = resources.get(textures.first))
                val.value()->bind();
            for (auto& colors : textures.second)
            {
                square_shader->setVec4("color", colors.first);
                for (auto& blend_factors : colors.second)
                {
                    square_shader->setVec4("use_texture", blend_factors.first);
                    for (auto& line_obj : blend_factors.second)
                    {
                        auto& line = line_obj.obj;
                        // 0, 1 (top right)
                        // 5, 6 (bottom right)
                        // 10, 11 (bottom left)
                        // 15, 16 (top left)
                        blt::vec2 dir = (line.p1 - line.p2).normalize();
                        blt::vec2 right = {dir.y(), -dir.x()};
                        blt::vec2 left = {-dir.y(), dir.x()};
                        
                        auto bottom_left = line.p1 + left * line.thickness;
                        auto bottom_right = line.p1 + right * line.thickness;
                        
                        auto top_left = line.p2 + left * line.thickness;
                        auto top_right = line.p2 + right * line.thickness;
                        
                        line_vertices[0] = top_right.x();
                        line_vertices[1] = top_right.y();
                        
                        line_vertices[5] = bottom_right.x();
                        line_vertices[6] = bottom_right.y();
                        
                        line_vertices[10] = bottom_left.x();
                        line_vertices[11] = bottom_left.y();
                        
                        line_vertices[15] = top_left.x();
                        line_vertices[16] = top_left.y();
                        
                        buf.update(sizeof(line_vertices), line_vertices);
                        
                        square_shader->setFloat("z_index", (line_obj.z_index - min) * denominator);
                        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
                        draw_count_++;
                    }
                    blend_factors.second.clear();
                }
                colors.second.clear();
            }
            textures.second.clear();
        }
        glDisable(GL_DEPTH_TEST);
        if (transparency)
            glDisable(GL_BLEND);
    }
}