/*
 *  <Short Description>
 *  Copyright (C) 2024  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/font_renderer.h>
#include <blt/gfx/renderer/shaders/2d_font.vert>
#include <blt/gfx/renderer/shaders/2d_font.frag>
#include "blt/gfx/window.h"

#include <blt/gfx/shader.h>
#include <blt/gfx/imgui/ImGuiUtils.h>
#include <cctype>

// TODO: signed distance fonts
//#include <msdfgen.h>

#include <utility>

namespace blt::gfx
{
    
    font_texture_atlas::font_texture_atlas(blt::i32 dimensions, blt::u32 padding): texture_gl2D(dimensions, dimensions, GL_R8), padding(padding)
    {
        bind();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
    }
    
    font_texture_atlas::added_font_results_t font_texture_atlas::add_font(const font::font_file_t& file, float size)
    {
        return add_font(file, file.character_min_index, size);
    }
    
    font_texture_atlas::added_font_results_t font_texture_atlas::add_font(const font::font_file_t& file, blt::u64 start, float size)
    {
        blt::u32 max_height = 0;
        blt::u64 min_index_used = file.character_max_index;
        blt::u64 max_index_used = start;
        file.font.set_pixel_sizes(0, static_cast<blt::u32>(size));
        
        for (auto i = start; i < file.character_max_index; i++)
        {
            // returns an empty optional if there is no error code
            if (file.font.load_char(i))
                continue;
            auto& face = *file.font.get();
            
            auto width = face->glyph->bitmap.width;
            auto height = face->glyph->bitmap.rows;
            
            max_height = std::max(max_height, height);
            
            // if adding this width overflows, we need to move to the next row.
            if (used_width + width > static_cast<blt::u32>(getWidth()))
            {
                used_height += max_height + padding;
                max_height = 0;
                used_width = 0;
            }
            
            // if we can't fit the height of this character we should move to the next texture atlas.
            if (used_height + height > static_cast<blt::u32>(getHeight()))
                return {i, min_index_used, max_index_used};
            min_index_used = std::min(min_index_used, i);
            max_index_used = std::max(max_index_used, i);
            
            auto begin_width = used_width;
            auto begin_height = used_height;
            auto end_width = used_width + width;
            auto end_height = used_height + height;
            used_width += width + padding;

//            BLT_TRACE("%d %d %d %d", begin_width, begin_height, width, height);
            // upload the texture
            upload(face->glyph->bitmap.buffer, GL_RED, 0, static_cast<blt::i32>(begin_width), static_cast<blt::i32>(begin_height),
                   static_cast<blt::i32>(width), static_cast<blt::i32>(height), GL_UNSIGNED_BYTE);
            
            glyphs[file.font.get_name()].insert(std::pair{i, bounded_t{
                    font::glyph_t{
                            {width, height},
                            {face->glyph->bitmap_left, face->glyph->bitmap_top},
                            face->glyph->advance.x},
                    {begin_width, begin_height}, {end_width, end_height}}
            });
        }
        bind();
        generateMipmaps();
        return {-1ul, min_index_used, max_index_used};
    }
    
    font_generator_t::font_generator_t(blt::i32 dimensions, blt::u32 padding, float gen_size):
            dimensions(dimensions), padding(padding), gen_size(gen_size)
    {}
    
    void font_generator_t::add_font(const font::font_file_t& file)
    {
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
        auto& vec = atlases;
        
        if (atlases.empty())
            atlases.emplace_back(std::make_unique<font_texture_atlas>(dimensions, padding));
        auto& font_vec = font_associations[file.font.get_name()];
        
        blt::u64 start_index = file.character_min_index;
        while (start_index != -1ul)
        {
            // add font and associate results with a font file
            auto results = atlases.back()->add_font(file, start_index, gen_size);
            font_vec.emplace_back(atlases.back().get(), results.min_index_used, results.max_index_used);
            // didn't fit, need to push new atlas
            if (results.last_inserted_index != -1ul)
                vec.emplace_back(std::make_unique<font_texture_atlas>(dimensions, padding));
            start_index = results.last_inserted_index;
        }
        glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
    }
    
    font_renderer_t::font_renderer_t() = default;
    
    void font_renderer_t::create(float gen_size, blt::i32 dimensions)
    {
        font_shader = shader_t::make_unique(shader_2d_font_vert, shader_2d_font_frag);
        font_shader->bind();
        font_shader->bindAttribute(0, "vertex");
        font::create();
        if (dimensions == 0)
        {
            int size;
            glGetIntegerv(GL_MAX_TEXTURE_SIZE, &size);
            dimensions = std::min(size, 2048);
            BLT_INFO("Max texture size %d, font renderer will use size %d", size, dimensions);
        }
        generator = std::make_unique<font_generator_t>(dimensions, 2, gen_size);
    }
    
    void font_renderer_t::create_default(float gen_size, blt::i32 dimensions)
    {
        create(gen_size, dimensions);

//        add_default_font(reinterpret_cast<const blt::u8*>(font::default_font_compressed_data), font::default_font_compressed_size, true);
        add_default_font(reinterpret_cast<const blt::u8*>(ImGui::Spectrum::SourceSansProRegular_compressed_data),
                         ImGui::Spectrum::SourceSansProRegular_compressed_size, true);
    }
    
    void font_renderer_t::cleanup()
    {
        for (auto& n : added_texts)
            n = nullptr;
        for (auto& c : cached_deallocated_text_objects)
            c = nullptr;
        added_texts.clear();
        cached_deallocated_text_objects.clear();
        text_ptr_to_index.clear();
        font_shader = nullptr;
        generator = nullptr;
        blt::gfx::font::cleanup();
    }
    
    font_renderer_t::compiled_text_t* font_renderer_t::create_text(std::string_view str, float size, std::string_view font)
    {
        std::unique_ptr<font_renderer_t::compiled_text_t> ptr = allocate_text();
        ptr->setText(str, size, font);
        auto index = added_texts.size();
        added_texts.push_back(std::move(ptr));
        text_ptr_to_index[added_texts.back().get()] = index;
        return added_texts.back().get();
    }
    
    void font_renderer_t::destroy_text(font_renderer_t::compiled_text_t* text)
    {
        auto index = text_ptr_to_index[text];
        text_ptr_to_index.erase(text);
        cached_deallocated_text_objects.push_back(std::move(added_texts[index]));
        added_texts[index] = nullptr;
        added_texts.erase(added_texts.begin() + static_cast<blt::ptrdiff_t>(index));
    }
    
    void font_renderer_t::render()
    {
#ifndef __EMSCRIPTEN__
        glDisable(GL_MULTISAMPLE);
#endif
        
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        font_shader->bind();
        glActiveTexture(GL_TEXTURE0);
        for (auto [text, index] : text_ptr_to_index)
        {
            // stupid hack
            if (text_ptr_to_text_index.find(text) != text_ptr_to_text_index.end())
                continue;
            text->bind();
            blt::mat4x4 transform;
            transform.translate(text->position);
            transform.scale(text->scale);
            transform.rotateZ(blt::toRadians(text->rotation));
            font_shader->setMatrix("transform", transform);
            font_shader->setFloat("z_index", text->z_index);
            font_shader->setVec4("color", text->color);
            text->draw();
        }
        
        for (auto& [text, list] : render_list)
        {
            text->bind();
            render_context_t before{text->current_size, text};
            text->save_to(before);
            for (const auto& context : list)
            {
                text->restore_from(context);
                
                blt::mat4x4 transform;
                transform.translate(text->position);
                transform.scale(text->scale);
                transform.rotateZ(blt::toRadians(text->rotation));
                font_shader->setMatrix("transform", transform);
                font_shader->setFloat("z_index", text->z_index);
                font_shader->setVec4("color", text->color);
                text->draw();
            }
            text->restore_from(before);
            
            if (last_rendered_strings.find(text) != last_rendered_strings.end())
                last_rendered_strings.erase(text);
        }
        render_list.clear();
        
        for (auto& text : last_rendered_strings)
        {
            rendered_strings.erase(text_ptr_to_text_index[text]);
            text_ptr_to_text_index.erase(text);
            cached_deallocated_text_objects.emplace_back(std::move(added_texts[text_ptr_to_index[text]]));
            text_ptr_to_index.erase(text);
        }
        last_rendered_strings.clear();
        
        for (auto& text : currently_rendered_strings)
            last_rendered_strings.insert(text);
        currently_rendered_strings.clear();

#ifndef __EMSCRIPTEN__
        glEnable(GL_MULTISAMPLE);
#endif
    }
    
    void font_renderer_t::add_default_font(std::string_view path, std::string_view name)
    {
        font::font_face_t default_font_face{path, name};
        font::font_file_t default_font{default_font_face, 0, 256};
        add_font(default_font);
    }
    
    void font_renderer_t::add_default_font(const blt::u8* data, blt::size_t size, bool compressed, std::string_view name)
    {
        font::font_face_t default_font_face{name, data, size, compressed};
        font::font_file_t default_font{default_font_face, 0, 256};
        add_font(default_font);
    }
    
    std::unique_ptr<font_renderer_t::compiled_text_t> font_renderer_t::allocate_text()
    {
        std::unique_ptr<font_renderer_t::compiled_text_t> ptr;
        if (!cached_deallocated_text_objects.empty())
        {
            ptr = std::move(*cached_deallocated_text_objects.begin());
            cached_deallocated_text_objects.erase(cached_deallocated_text_objects.begin());
        } else
        {
            ptr = std::unique_ptr<compiled_text_t>(new compiled_text_t{*generator});
        }
        return ptr;
    }
    
    font_renderer_t::render_context_t& font_renderer_t::render_text(std::string_view str, float size, std::string_view font)
    {
        auto str_iter = rendered_strings.find(text_index_t{std::string(str), std::string(font)});
        compiled_text_t* ptr;
        if (str_iter != rendered_strings.end())
        {
            ptr = str_iter->second;
            currently_rendered_strings.insert(ptr);
            ptr->setSize(size);
        } else
        {
            auto index = added_texts.size();
            added_texts.emplace_back(allocate_text());
            ptr = added_texts.back().get();
            ptr->setText(str, size);
            
            text_ptr_to_text_index[ptr] = text_index_t{std::string(str), std::string(font)};
            rendered_strings[text_index_t{std::string(str), std::string(font)}] = ptr;
            text_ptr_to_index[ptr] = index;
            
            currently_rendered_strings.insert(ptr);
        }
        
        auto& vec = render_list[ptr];
        vec.emplace_back(size, ptr);
        return vec.back();
    }
    
    font_renderer_t::compiled_text_t& font_renderer_t::compiled_text_t::setText(std::string_view str, float size, std::string_view f)
    {
        if (str == contents)
            return *this;
        
        contents = str;
        font = f;
        setSize(size);
        recompile();
        return *this;
    }
    
    font_renderer_t::compiled_text_t::compiled_text_t(font_generator_t& generator): generator(generator)
    {
        auto vbo = vertex_array_t::make_vbo({});
        vbo->vbo.create();
        vbo->vbo.bind();
        vbo->vbo.allocate(sizeof(float) * 6 * 4, GL_DYNAMIC_DRAW);
        vao.bind();
        vao.bindVBO(vbo, 0, 4, GL_FLOAT, 4 * sizeof(float), 0);
    }
    
    void font_renderer_t::compiled_text_t::draw()
    {
        font_texture_atlas* last_texture = nullptr;
        for (const auto& render : renders)
        {
            if (last_texture != render.texture)
            {
                last_texture = render.texture;
                render.texture->bind();
            }
            
            glDrawArrays(GL_TRIANGLES, static_cast<int>(render.render_start), static_cast<int>(render.render_count));
        }
    }
    
    font_renderer_t::compiled_text_t& font_renderer_t::compiled_text_t::recompile()
    {
        static std::vector<float> vertices;
        vertices.clear();
        renders.clear();
        
        font_texture_atlas* last_texture = nullptr;
        blt::size_t draw_start = 0;
        blt::size_t draw_count = 0;
        
        const float sc = generator.get_generated_size() / current_size;
        
        float global_x = 0;
        float global_y = 0;
        // TODO: parker UTF8
        for (const auto& c : contents)
        {
            if (bounds.y() > 0 && std::abs(global_y) >= bounds.y() * sc)
                continue;
            if (c == '\n')
            {
                global_x = 0;
                global_y -= generator.get_generated_size();
                continue;
            }
            auto& texture = generator.get_texture(font, c);
            auto& ch = texture.get_glyph(c, font);
            
            if (std::isblank(c))
            {
                if (bounds.x() > 0 && global_x > bounds.x() * sc)
                {
                    global_x = 0;
                    global_y -= generator.get_generated_size();
                    continue;
                }
            }
            
            if (last_texture == nullptr)
                last_texture = &texture;
            else if (last_texture != &texture)
            {
                renders.emplace_back(last_texture, draw_start, draw_count);
                draw_start += draw_count;
                draw_count = 0;
            }
            
            auto dims = static_cast<float>(generator.get_dimensions());
            
            float x_pos = global_x + static_cast<float>(ch.glyph.bearing.x());
            float y_pos = global_y - static_cast<float>(ch.glyph.size.y() - ch.glyph.bearing.y());
            
            auto w = static_cast<float>(ch.glyph.size.x());
            auto h = static_cast<float>(ch.glyph.size.y());
            
            auto texture_min_x = static_cast<float>(ch.min.x()) / dims;
            auto texture_min_y = static_cast<float>(ch.min.y()) / dims;
            auto texture_max_x = static_cast<float>(ch.max.x()) / dims;
            auto texture_max_y = static_cast<float>(ch.max.y()) / dims;

//            BLT_TRACE("%c: %f | xy[%f %f] wh[%f %f] | min[%f %f] max[%f %f] |", c, x, x_pos, y_pos, w, h, texture_min_x, texture_min_y, texture_max_x, texture_max_y);
            
            std::array<float, 6 * 4> local_vert = {
                    x_pos, y_pos + h, texture_min_x, texture_min_y,
                    x_pos, y_pos, texture_min_x, texture_max_y,
                    x_pos + w, y_pos, texture_max_x, texture_max_y,
                    
                    x_pos, y_pos + h, texture_min_x, texture_min_y,
                    x_pos + w, y_pos, texture_max_x, texture_max_y,
                    x_pos + w, y_pos + h, texture_max_x, texture_min_y
            };
            vertices.insert(vertices.end(), local_vert.begin(), local_vert.end());
            
            draw_count += 6;
            global_x += static_cast<float>(ch.glyph.advance >> 6);
        }
        text_width = global_x * scale.x();
        text_height = -(global_y - generator.get_generated_size()) * scale.y();

//        BLT_TRACE("size: %ld %ld %ld", draw_count, vertices.size(), vertices.size() / 4);
        
        renders.emplace_back(last_texture, draw_start, draw_count);
        vao.getBuffer(0).update(static_cast<long>(vertices.size() * sizeof(float)), vertices.data());
        return *this;
    }
    
    
    bool font_renderer_t::text_index_t::operator==(const font_renderer_t::text_index_t& rhs) const
    {
        return text == rhs.text && font == rhs.font;
    }
    
    bool font_renderer_t::text_index_t::operator!=(const font_renderer_t::text_index_t& rhs) const
    {
        return !(rhs == *this);
    }
}