#pragma once
/*
 *  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/>.
 */

#ifndef BLT_WITH_GRAPHICS_FONT_RENDERER_H
#define BLT_WITH_GRAPHICS_FONT_RENDERER_H

#include <blt/gfx/font/font.h>
#include <blt/gfx/texture.h>
#include <blt/gfx/shader.h>
#include <blt/gfx/model.h>
#include <blt/gfx/framebuffer.h>
#include <blt/std/hashmap.h>
#include <blt/std/binary_tree.h>

namespace blt::gfx
{
    
    class font_texture_atlas : public texture_gl2D
    {
        public:
            struct bounded_t
            {
                font::glyph_t glyph;
                // starting point inside texture atlas
                blt::vec2ui min;
                // ending point of the glyph, inside texture atlas
                blt::vec2ui max;
            };
            struct added_font_results_t
            {
                blt::u64 last_inserted_index;
                blt::u64 min_index_used;
                blt::u64 max_index_used;
            };
        public:
            explicit font_texture_atlas(blt::i32 dimensions, blt::u32 padding);
            
            added_font_results_t add_font(const font::font_file_t& file, float size);
            
            added_font_results_t add_font(const font::font_file_t& file, blt::u64 start, float size);
            
            bounded_t& get_glyph(blt::u64 c, std::string_view font)
            { return glyphs.at(font).at(c); }
            
            [[nodiscard]] const bounded_t& get_glyph(blt::u64 c, std::string_view font) const
            { return glyphs.at(font).at(c); }
        
        private:
            blt::hashmap_t<std::string, blt::hashmap_t<blt::u64, bounded_t>> glyphs;
            blt::u32 used_width = 0;
            blt::u32 used_height = 0;
            blt::u32 padding;
    };
    
    class font_generator_t
    {
        public:
            
            struct size_bounds_t
            {
                float min_size, max_size;
                blt::u64 min_char, max_char;
            };
            
            struct bounded_font_t
            {
                font_texture_atlas* atlas;
                blt::u64 min_char, max_char;
                
                bounded_font_t(font_texture_atlas* atlas, u64 minChar, u64 maxChar): atlas(atlas), min_char(minChar), max_char(maxChar)
                {}
                // replace with font -> char range. use scaling.
            };
        
        public:
            explicit font_generator_t(blt::i32 dimensions, blt::u32 padding, float gen_size);
            
            void add_font(const font::font_file_t& file);
            
            inline font_texture_atlas& get_texture(const std::string& font, blt::u64 c)
            {
                for (auto& atlas : font_associations[font])
                {
                    if (c >= atlas.min_char && c <= atlas.max_char)
                        return *atlas.atlas;
                }
                throw std::runtime_error("Character '" + std::to_string(c) + "' not found inside atlases with font '" + font + "'");
            }
            
            [[nodiscard]] blt::i32 get_dimensions() const
            { return dimensions; }
            
            [[nodiscard]] float get_generated_size() const
            { return gen_size; }
        
        private:
            blt::i32 dimensions;
            blt::u32 padding;
            std::vector<std::unique_ptr<font_texture_atlas>> atlases;
            blt::hashmap_t<std::string, std::vector<bounded_font_t>> font_associations;
            float gen_size;
    };
    
    class font_renderer_t
    {
        public:
            static constexpr blt::vec2f DEFAULT_SCALE{1, 1};
            static constexpr blt::vec4 DEFAULT_COLOR = blt::make_color(0.8, 0.8, 0.8);
            
            struct compiled_text_t;
            
            struct render_context_t
            {
                blt::vec2f position, bounds;
                blt::vec4 color = DEFAULT_COLOR;
                float z_index = 0;
                float current_size = 0;
                float rotation = 0;
                
                compiled_text_t* text_ptr;
                
                explicit render_context_t(float current_size, compiled_text_t* associated_text): current_size(current_size), text_ptr(associated_text)
                {}
                
                [[nodiscard]] compiled_text_t& getAssociatedText() const
                { return *text_ptr; }
                
                render_context_t& setPosition(float x, float y)
                {
                    position = {x, y};
                    return *this;
                }
                
                render_context_t& setPosition(const blt::vec2f& new_position)
                {
                    position = new_position;
                    return *this;
                }
                
                render_context_t& setRotation(float r)
                {
                    rotation = r;
                    return *this;
                }
                
                render_context_t& setColor(const blt::vec4f& new_color)
                {
                    color = new_color;
                    return *this;
                }
                
                render_context_t& setColor(float r, float g, float b, float a = 1)
                {
                    color = {r, g, b, a};
                    return *this;
                }
                
                render_context_t& setBounds(const blt::vec2f& new_bounds)
                {
                    bounds = new_bounds;
                    return *this;
                }
                
                render_context_t& setBounds(float width, float height)
                {
                    return setBounds({width, height});
                }
                
                render_context_t& setUnrestrictedBounds()
                {
                    bounds = {};
                    return *this;
                }
                
                render_context_t& setSize(const float new_scale)
                {
                    current_size = new_scale;
                    return *this;
                }
                
                render_context_t& setZIndex(float new_z_index)
                {
                    z_index = new_z_index;
                    return *this;
                }
                
                [[nodiscard]] const vec2f& getPosition() const
                { return position; }
                
                [[nodiscard]] const vec2f& getBounds() const
                { return bounds; }
                
                [[nodiscard]] float getSize() const
                { return current_size; }
                
                [[nodiscard]] float getRotation() const
                { return rotation; }
                
                [[nodiscard]] const vec4& getColor() const
                { return color; }
            };
            
            struct text_index_t
            {
                std::string text;
                std::string font;
                
                bool operator==(const text_index_t& rhs) const;
                
                bool operator!=(const text_index_t& rhs) const;
            };
            
            struct compiled_text_t
            {
                    friend font_renderer_t;
                public:
                    compiled_text_t& setText(std::string_view str, float size, std::string_view font = "__default");
                    
                    compiled_text_t& recompile();
                    
                    compiled_text_t& setPosition(float x, float y)
                    {
                        position = {x, y};
                        return *this;
                    }
                    
                    compiled_text_t& setPosition(const blt::vec2f& pos)
                    {
                        position = pos;
                        return *this;
                    }
                    
                    compiled_text_t& setRotation(float r)
                    {
                        rotation = r;
                        return *this;
                    }
                    
                    compiled_text_t& setBounds(const blt::vec2f& limit)
                    {
                        if (limit == bounds)
                            return *this;
                        bounds = limit;
                        recompile();
                        return *this;
                    }
                    
                    compiled_text_t& setBounds(float width, float height)
                    {
                        return setBounds({width, height});
                    }
                    
                    compiled_text_t& setUnrestrictedBounds()
                    {
                        bounds = {};
                        return *this;
                    }
                    
                    compiled_text_t& setColor(const blt::vec4f& c)
                    {
                        color = c;
                        return *this;
                    }
                    
                    compiled_text_t& setColor(float r, float g, float b, float a = 1)
                    {
                        color = {r, g, b, a};
                        return *this;
                    }
                    
                    compiled_text_t& setSize(float size)
                    {
                        if (current_size == size)
                            return *this;
                        current_size = size;
                        scale = {size / generator.get_generated_size(), size / generator.get_generated_size()};
                        // size only changes computation if there are bounds on the text.
                        if (bounds.x() != 0 || bounds.y() != 0)
                            recompile();
                        return *this;
                    }
                    
                    compiled_text_t& setZIndex(float s)
                    {
                        z_index = s;
                        return *this;
                    }
                    
                    [[nodiscard]] const vec2f& getBounds() const
                    { return bounds; }
                    
                    [[nodiscard]] const vec2f& getPosition() const
                    { return position; }
                    
                    [[nodiscard]] const vec2f& getScale() const
                    { return scale; }
                    
                    [[nodiscard]] const vec4f& getColor() const
                    { return color; }
                    
                    [[nodiscard]] float getZIndex() const
                    { return z_index; }
                    
                    [[nodiscard]] float getRotation() const
                    { return rotation; }
                    
                    [[nodiscard]] float getTextWidth() const
                    { return text_width; }
                    
                    [[nodiscard]] float getTextHeight() const
                    { return text_height; }
                    
                    void save_to(render_context_t& save)
                    {
                        save.position = position;
                        save.bounds = bounds;
                        save.color = color;
                        save.z_index = z_index;
                        save.current_size = current_size;
                        save.rotation = rotation;
                    }
                    
                    void restore_from(const render_context_t& restore)
                    {
                        setPosition(restore.position);
                        setBounds(restore.bounds);
                        setColor(restore.color);
                        setZIndex(restore.z_index);
                        setSize(restore.current_size);
                        setRotation(restore.rotation);
                    }
                
                private:
                    explicit compiled_text_t(font_generator_t& generator);
                    
                    void bind() const
                    {
                        vao.bind();
                    }
                    
                    void draw();
                    
                    struct text_render_info_t
                    {
                        font_texture_atlas* texture = nullptr;
                        blt::size_t render_start = 0;
                        blt::size_t render_count = 0;
                        
                        text_render_info_t(font_texture_atlas* texture, size_t renderStart, size_t renderCount):
                                texture(texture), render_start(renderStart), render_count(renderCount)
                        {}
                    };
                    
                    font_generator_t& generator;
                    std::string contents, font;
                    vertex_array_t vao{};
                    std::vector<text_render_info_t> renders;
                    blt::vec2f position, bounds;
                    blt::vec2f scale = DEFAULT_SCALE;
                    blt::vec4 color = DEFAULT_COLOR;
                    float z_index = 0;
                    float current_size = 0;
                    float rotation = 0;
                    float text_width = 0;
                    float text_height = 0;
            };
        
        public:
            explicit font_renderer_t();
            
            void create_default(float gen_size = 250, blt::i32 dimensions = 0);
            
            void create(float gen_size, blt::i32 dimensions = 0);
            
            void cleanup();
            
            void add_font(const font::font_file_t& file)
            {
                generator->add_font(file);
            }
            
            void add_default_font(std::string_view path, std::string_view name = "__default");
            
            void add_default_font(const blt::u8* data, blt::size_t size, bool compressed = false, std::string_view name = "__default");
            
            compiled_text_t* create_text(std::string_view str, float size, std::string_view font = "__default");
            
            render_context_t& render_text(std::string_view str, float size, std::string_view font = "__default");
            
            void destroy_text(compiled_text_t* text);
            
            void render();
        
        private:
            std::unique_ptr<compiled_text_t> allocate_text();
            
            std::unique_ptr<font_generator_t> generator;
            std::unique_ptr<shader_t> font_shader;
            
            blt::hashmap_t<text_index_t, compiled_text_t*> rendered_strings;
            blt::hashmap_t<compiled_text_t*, text_index_t> text_ptr_to_text_index;
            blt::hashmap_t<compiled_text_t*, std::vector<render_context_t>> render_list;
            
            blt::hashset_t<compiled_text_t*> last_rendered_strings;
            blt::hashset_t<compiled_text_t*> currently_rendered_strings;
            
            blt::hashmap_t<compiled_text_t*, blt::size_t> text_ptr_to_index;
            std::vector<std::unique_ptr<compiled_text_t>> added_texts;
            std::vector<std::unique_ptr<compiled_text_t>> cached_deallocated_text_objects;
    };
    
}

namespace blt::gfx::detail
{
    inline void hash_combine(std::size_t& seed, const std::size_t& value)
    {
        seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    }
}

// Specialization of std::hash for text_index_t
template<>
struct std::hash<blt::gfx::font_renderer_t::text_index_t>
{
    std::size_t operator()(const blt::gfx::font_renderer_t::text_index_t& key) const
    {
        blt::size_t hash = 0;
        blt::gfx::detail::hash_combine(hash, std::hash<std::string>{}(key.text));
        blt::gfx::detail::hash_combine(hash, std::hash<std::string>{}(key.font));
        
        return hash;
    }
};

#endif //BLT_WITH_GRAPHICS_FONT_RENDERER_H
