/*
 *  <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/shaders/2d_textured.vert>
#include <blt/gfx/renderer/shaders/2d_textured.frag>
#include <blt/gfx/renderer/shaders/2d_textured_circle.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()
    {
        {
            vertex_buffer_t vertices_vbo;
            element_buffer_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 = std::make_unique<vertex_array_t>();
            auto vb = vertex_array_t::make_vbo(vertices_vbo);
            square_vao->bindVBO(vb, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
            square_vao->bindVBO(vb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
            square_vao->bindElement(indices_vbo);
        }
        {
            vertex_buffer_t vertices_vbo;
            element_buffer_t indices_vbo;
            
            vertices_vbo.create();
            indices_vbo.create();
            
            vertices_vbo.allocate(sizeof(line_vertices), line_vertices, GL_DYNAMIC_DRAW);
            indices_vbo.allocate(sizeof(square_indices), square_indices, GL_DYNAMIC_DRAW);
            
            line_vao = std::make_unique<vertex_array_t>();
            auto vb = vertex_array_t::make_vbo(vertices_vbo);
            line_vao->bindVBO(vb, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
            line_vao->bindVBO(vb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
            line_vao->bindElement(indices_vbo);
        }
        
        square_shader = shader_t::make_unique(shader_2d_textured_vert, shader_2d_textured_frag);
        square_shader->bindAttribute(0, "vertex");
        square_shader->bindAttribute(1, "uv_in");
        
        point_shader = shader_t::make_unique(shader_2d_textured_vert, shader_2d_textured_cirlce_frag);
        point_shader->bindAttribute(0, "vertex");
        point_shader->bindAttribute(1, "uv_in");
        
        engine->create();
    }
    
    void batch_renderer_2d::drawRectangleInternal(const std::string_view texture, const rectangle2d_t& rectangle, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_rectangles, render_info_t::make_info(texture), {-z_index, rectangle});
    }
    
    void batch_renderer_2d::drawRectangleInternal(const vec4& color, const rectangle2d_t& rectangle, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_rectangles, render_info_t::make_info(color), {-z_index, rectangle});
    }
    
    void batch_renderer_2d::drawRectangleInternal(const render_info_t& draw_info, const rectangle2d_t& rectangle, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_rectangles, draw_info, {-z_index, rectangle});
    }
    
    void batch_renderer_2d::drawLineInternal(const std::string_view texture, const line2d_t& line, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_lines, render_info_t::make_info(texture), {-z_index, line});
    }
    
    void batch_renderer_2d::drawLineInternal(const vec4& color, const line2d_t& line, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_lines, render_info_t::make_info(color), {-z_index, line});
    }
    
    void batch_renderer_2d::drawLineInternal(const render_info_t& draw_info, const line2d_t& line, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_lines, draw_info, {-z_index, line});
    }
    
    void batch_renderer_2d::drawPointInternal(const std::string_view texture, const point2d_t& point, const f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_points, render_info_t::make_info(texture), {-z_index, point});
    }
    
    void batch_renderer_2d::drawPointInternal(const vec4& color, const point2d_t& point, f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_points, render_info_t::make_info(color), {-z_index, point});
    }
    
    void batch_renderer_2d::drawPointInternal(const render_info_t& draw_info, const point2d_t& point, f32 z_index)
    {
        update_z_index(z_index);
        insert_obj(draw.complex_points, draw_info, {-z_index, point});
    }
    
    void batch_renderer_2d::cleanup()
    {
        engine->cleanup();
        engine = nullptr;
        square_vao = nullptr;
        line_vao = nullptr;
        square_shader = nullptr;
        point_shader = nullptr;
    }
    
    void batch_renderer_2d::render(i32, i32, const bool transparency)
    {
        //draw_buffer.bind();
        //draw_buffer.updateBuffersStorage(width, height);
        //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        if (transparency)
        {
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        }
        glEnable(GL_DEPTH_TEST);
        glActiveTexture(GL_TEXTURE0);
        
        engine->bind();
        draw_objects();
        
        glDisable(GL_DEPTH_TEST);
        if (transparency)
            glDisable(GL_BLEND);
        
        engine->render();
        render_reset();
    }
    
    void batch_renderer_2d::draw_objects()
    {
        pre_reset();
        const f32 denominator = 1.0f / (draw.z_max - draw.z_min);
        
        glEnable(GL_STENCIL_TEST);
        glClear(GL_STENCIL_BUFFER_BIT);
        glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
        glStencilMask(0xFF);
        
        glStencilFunc(GL_ALWAYS, 1, 0xFF);
        draw_points(false, denominator);
        glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
        glStencilMask(0x00);
        glDisable(GL_DEPTH_TEST);
        draw_points(true, denominator);
        glEnable(GL_DEPTH_TEST);
        
        draw_rectangles(false, denominator);
        draw_lines(false, denominator);
        
        glDisable(GL_STENCIL_TEST);
        
        post_reset();
    }
    
    void batch_renderer_2d::render_reset()
    {
        draw.z_min = std::numeric_limits<f32>::max();
        draw.z_max = std::numeric_limits<f32>::min();
    }
    
    void batch_renderer_2d::pre_reset()
    {
        draw.draw_count = 0;
    }
    
    void batch_renderer_2d::post_reset()
    {
    
    }
    
    void batch_renderer_2d::draw_points(const bool outlined, const f32 denominator)
    {
        point_shader->bind();
        square_vao->bind();
        for (auto& [texture, objects] : draw.complex_points)
        {
            // resource manager handles the check for empty string
            if (auto val = resources.get(texture))
                val.value()->bind();
            for (auto& [render_info, object] : objects)
            {
                auto& [z_index, point] = object;
                
                mat4x4 model;
                model.translate(point.pos.x(), point.pos.y(), 0.0f);
                if (outlined)
                {
                    BLT_TRACE(render_info.outline_thickness);
                    model.scale(point.scale * render_info.outline_thickness, point.scale * render_info.outline_thickness, 1.0);
                    point_shader->setVec4("color", render_info.outline_color);
                    point_shader->setVec4("use_texture", blt::vec4{0,0,0,0});
                } else
                {
                    model.scale(point.scale, point.scale, 1.0);
                    point_shader->setVec4("color", render_info.color);
                    point_shader->setVec4("use_texture", render_info.blend);
                }
                
                
                point_shader->setVec4("outline_color", render_info.outline_color);
                point_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);
                point_shader->setMatrix("model", model);
                
                glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
                draw.draw_count++;
            }
            objects.clear();
        }
    }
    
    void batch_renderer_2d::draw_lines(bool outlined, const f32 denominator)
    {
        mat4x4 empty_model;
        square_shader->setMatrix("model", empty_model);
        line_vao->bind();
        auto& buf = line_vao->getBuffer(0);
        buf.bind();
        for (auto& [texture, objects] : draw.complex_lines)
        {
            // resource manager handles the check for empty string
            if (auto val = resources.get(texture))
                val.value()->bind();
            for (auto& [render_info, object] : objects)
            {
                auto& [z_index, line] = object;
                
                square_shader->setVec4("color", render_info.color);
                square_shader->setVec4("use_texture", render_info.blend);
                square_shader->setVec4("outline_color", render_info.outline_color);
                square_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);
                
                // 0, 1 (top right)
                // 5, 6 (bottom right)
                // 10, 11 (bottom left)
                // 15, 16 (top left)
                vec2 dir = (line.p1 - line.p2).normalize();
                vec2 right = {dir.y(), -dir.x()};
                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);
                
                glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
                draw.draw_count++;
            }
            objects.clear();
        }
    }
    
    void batch_renderer_2d::draw_rectangles(bool outlined, const f32 denominator)
    {
        square_shader->bind();
        square_vao->bind();
        for (auto& [texture, objects] : draw.complex_rectangles)
        {
            // resource manager handles the check for empty string
            if (auto val = resources.get(texture))
                val.value()->bind();
            for (auto& [render_info, object] : objects)
            {
                auto& [z_index, rect] = object;
                
                mat4x4 model;
                if (outlined) {
                    auto nsx = rect.size.x() * render_info.outline_thickness;
                    auto nsy = rect.size.y() * render_info.outline_thickness;
                    auto ns = blt::vec2(nsx, nsy);
                    auto s = (rect.size - ns) / 2.0f;
                    model.translate(rect.pos - s);
                    model.scale(ns);
                    square_shader->setVec4("color", render_info.outline_color);
                    square_shader->setVec4("use_texture", {0,0,0,0});
                } else {
                    model.translate(rect.pos);
                    model.scale(rect.size);
                    square_shader->setVec4("color", render_info.color);
                    square_shader->setVec4("use_texture", render_info.blend);
                }
                if (rect.rotation != 0)
                    model.rotateZ(toRadians(rect.rotation));
                
                square_shader->setVec4("outline_color", render_info.outline_color);
                square_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);
                square_shader->setMatrix("model", model);
                glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
                draw.draw_count++;
            }
            objects.clear();
        }
    }
}
