#include "pch.h"
#include "log.h"
#include "font.h"
#include "legacy_gl_mesh_dispatch.h"
#include "legacy_ui_gl_dispatch.h"
#include "shader.h"
#include "asset.h"
#include "util.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"

#include <array>
#include <cstddef>
#include <cstdint>

namespace {

[[nodiscard]] GLint font_atlas_internal_format() noexcept
{
    return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).internal_format);
}

[[nodiscard]] GLint font_atlas_pixel_format() noexcept
{
    return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).pixel_format);
}

void activate_text_texture_unit(std::uint32_t unit_index)
{
    pp::legacy::ui_gl::activate_texture_unit(unit_index, "Text");
}

[[nodiscard]] std::span<const pp::renderer::gl::OpenGlVertexAttribute> text_mesh_vertex_attributes() noexcept
{
    static const std::array<pp::renderer::gl::OpenGlVertexAttribute, 2> attributes {
        pp::renderer::gl::OpenGlVertexAttribute {
            .index = 0U,
            .component_count = 2,
            .component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
            .normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
            .stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
            .offset = 0U,
        },
        pp::renderer::gl::OpenGlVertexAttribute {
            .index = 1U,
            .component_count = 2,
            .component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
            .normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
            .stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
            .offset = static_cast<std::uintptr_t>(sizeof(float) * 2),
        },
    };
    return attributes;
}

}

std::map<std::string, Font> FontManager::m_fonts;
Sampler FontManager::m_sampler;

Font::Font(Font&& other) noexcept
{
    w = other.w;
    h = other.h;
    bounds = other.bounds;
    size = other.size;
    scale = other.size;
    font_tex = std::move(other.font_tex);
    path = std::move(other.path);
    id = std::move(other.id);
    chars = std::move(other.chars);
}

void Font::operator=(Font&& other) noexcept
{
    w = other.w;
    h = other.h;
    bounds = other.bounds;
    size = other.size;
    scale = other.size;
    font_tex = std::move(other.font_tex);
    path = std::move(other.path);
    id = std::move(other.id);
    chars = std::move(other.chars);
}

bool Font::load(const std::string& ttf, int font_size, float font_scale)
{
    Asset file;
    LOG("Font::load %s", ttf.c_str());
    if (file.open(ttf.c_str()) && file.read_all())
    {
        w = h = 512 * (int)ceilf(font_scale);
        path = ttf;
        scale = font_scale;
        LOG("Font::load loaded");
        auto bitmap = std::make_unique<uint8_t[]>(w*h);
        chars.resize(num_chars);
        //int offset = stbtt_FindMatchingFont(file.m_data, "Arial Bold", STBTT_MACSTYLE_DONTCARE);
        //if (offset < 0)
        //    offset = 0;
        stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
        calc_bounds();
        font_tex.create(w, h, font_atlas_internal_format(), font_atlas_pixel_format(), bitmap.get());
        file.close();
        size = font_size;
        return true;
    }
    return false;
}

void Font::change_scale(float scale)
{
    load(path, size, scale);
}

void Font::calc_bounds()
{
    glm::vec2 bbmin(FLT_MAX);
    glm::vec2 bbmax(-FLT_MAX);
    for (int i = 0; i < num_chars; i++)
    {
        stbtt_aligned_quad q;
        float x = 0, y = 0;
        stbtt_GetBakedQuad(chars.data(), w, h, i, &x, &y, &q, true);
        bbmin = glm::min(bbmin, { q.x0 / scale, q.y0 / scale });
        bbmax = glm::max(bbmax, { q.x1 / scale, q.y1 / scale });
    }
    bounds = { glm::vec4(bbmin, bbmax) };
}

void FontManager::init()
{
    m_sampler.create();
}

bool FontManager::load(const std::string& id, const std::string& ttf, int sz, float scale)
{
    return m_fonts[id].load(ttf, sz, scale);
}

const Font& FontManager::get(const std::string& name, int size, const std::string& weight, bool italic)
{
    auto id = fmt::format("{}-{}{}-{}", name, weight, italic ? "-italic" : "", size);
    auto it = m_fonts.find(id);
    if (it == m_fonts.end())
    {
        LOG("load font %s", id.c_str());
        auto path = fmt::format("data/fonts/{}-{}{}.ttf", name, weight, italic ? "-italic" : "");
        m_fonts[id].load(path, size, App::I->display_density * App::I->zoom);
        return m_fonts[id];
    }
    return it->second;
}

void FontManager::change_scale(float scale)
{
    for (auto& f : m_fonts)
        f.second.change_scale(scale);
}

std::vector<TextMesh::Token> TextMesh::tokenize(const std::string& s, const Font& f) const noexcept
{
    std::vector<std::string> parts;
    std::array<char, 10> delims = { ' ', '\n', ',', '.', ':', ';', '?', '!', '\\', '/' };
    std::string tmp;
    bool wrap = false;
    for (char c : s)
    {
        bool is_delim = std::find(delims.begin(), delims.end(), c) != delims.end();
        wrap |= is_delim; // set wrap to notify a delimiter has been reached
        // when a new non-delim char is detected, start a new token
        if (wrap && !is_delim && !tmp.empty())
        {
            parts.push_back(tmp);
            tmp.clear();
            wrap = false;
        }
        tmp.push_back(c);
    }
    // insert last partial token
    if (!tmp.empty())
        parts.push_back(tmp);
    // measure each token length
    std::vector<TextMesh::Token> ret;
    for (auto p : parts)
    {
        float x = 0;
        float y = 0;
        for (char c : p)
        {
            if ((uint8_t)c >= 32 && (uint8_t)c < 256) // visible ascii character
            {
                stbtt_aligned_quad q;
                stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &x, &y, &q, true);
            }
        }
        ret.emplace_back(p, x);
    }
    return ret;
}

bool TextMesh::create()
{
    App::I->render_task([this]
    {
        const auto mesh = pp::legacy::gl_mesh::create_mesh_objects(
            pp::renderer::gl::OpenGlMeshUpload {
                .vertex_data = nullptr,
                .vertex_byte_count = 0,
                .index_data = nullptr,
                .index_byte_count = 0,
                .indexed = true,
                .vertex_array_count = 1U,
                .attributes = text_mesh_vertex_attributes(),
            },
            "TextMesh::create");
        if (mesh.vertex_buffer != 0U) {
            font_buffers[0] = static_cast<GLuint>(mesh.vertex_buffer);
            font_buffers[1] = static_cast<GLuint>(mesh.index_buffer);
            font_array = static_cast<GLuint>(mesh.vertex_arrays[0]);
        }
    });
    return true;
}

void TextMesh::update(const std::string& text, const std::string& font, int size, const std::string& weight, bool italic)
{
    this->font = font;
    this->size = size;
    this->weight = weight;
    this->italic = italic;

    auto& f = FontManager::get(font, size, weight, italic);
    float spacing = (f.bounds.w - f.bounds.y);
    float avg_width = f.bounds.z - f.bounds.x;
    cur_box = glm::vec4(0, f.bounds.y, 5, spacing);

    glm::vec2 bbmin(FLT_MAX);
    glm::vec2 bbmax(-FLT_MAX);
    if (text.empty())
    {
        bbmin = { 0, -spacing * 0.5f };
        bbmax = { 1, +spacing * 0.5f };
    }

    if (f.chars.size())
    {
        float x = 0;
        float y = 0;
        std::vector<glm::vec4> v;
        std::vector<GLushort> idx;

        std::vector<Token> parts = max_width > 0 ? 
            tokenize(text, f) : 
            std::vector<Token>{ Token(text, 0.f) };

        for (size_t i = 0; i < parts.size(); i++)
        {
            const auto& p = parts[i];
            if (i > 0 && max_width > 0 && (x + p.w) > max_width * f.scale)
            {
                x = 0;
                y += spacing * f.scale;
            }
            for (char c : p.s)
            {
                if (c == '\n')
                {
                    x = 0;
                    y += spacing * f.scale;
                    cur_box = glm::vec4(x, y / f.scale + f.bounds.y, 5, spacing);
                    continue;
                }
                stbtt_aligned_quad q;
                float wrap_test_x = x;
                stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &wrap_test_x, &y, &q, true);
                if (max_width > 0 && q.x1 > max_width * f.scale)
                {
                    x = 0;
                    y += spacing * f.scale;
                }
                stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &x, &y, &q, true);
                auto n = (int)v.size();
                v.emplace_back(q.x0 / f.scale, q.y1 / f.scale, q.s0, q.t1);
                v.emplace_back(q.x0 / f.scale, q.y0 / f.scale, q.s0, q.t0);
                v.emplace_back(q.x1 / f.scale, q.y0 / f.scale, q.s1, q.t0);
                v.emplace_back(q.x1 / f.scale, q.y1 / f.scale, q.s1, q.t1);
                idx.push_back(n + 0);
                idx.push_back(n + 1);
                idx.push_back(n + 2);
                idx.push_back(n + 0);
                idx.push_back(n + 2);
                idx.push_back(n + 3);
                bbmin = glm::min(bbmin, glm::floor(xy(f.bounds)));
                bbmax = glm::max(bbmax, glm::ceil(glm::vec2(q.x1 / f.scale, y / f.scale + f.bounds.w)));
                if (max_width > 0 && q.x1 / f.scale + 5 > max_width * f.scale)
                    cur_box = glm::vec4(0, (y + spacing * f.scale) / f.scale + f.bounds.y, 5, spacing);
                else
                    cur_box = glm::vec4(q.x1 / f.scale + 2, y / f.scale + f.bounds.y, 5, spacing);
            }
        }
        for (auto& vi : v)
            vi -= glm::vec4(xy(f.bounds), 0, 0);
        cur_box -= glm::vec4(xy(f.bounds), 0, 0);
        bb = bbmax - bbmin;
        font_array_count = (int)idx.size();
        App::I->render_task([&]
        {
            (void)pp::legacy::gl_mesh::upload_buffer_data(
                pp::renderer::gl::OpenGlBufferUpload {
                    .target = pp::renderer::gl::element_array_buffer_target(),
                    .buffer_id = font_buffers[1],
                    .data = idx.data(),
                    .byte_count = static_cast<std::intptr_t>(idx.size() * sizeof(GLushort)),
                    .usage = pp::renderer::gl::static_draw_buffer_usage(),
                },
                "TextMesh::update indices");
            (void)pp::legacy::gl_mesh::upload_buffer_data(
                pp::renderer::gl::OpenGlBufferUpload {
                    .target = pp::renderer::gl::array_buffer_target(),
                    .buffer_id = font_buffers[0],
                    .data = v.data(),
                    .byte_count = static_cast<std::intptr_t>(v.size() * sizeof(glm::vec4)),
                    .usage = pp::renderer::gl::static_draw_buffer_usage(),
                },
                "TextMesh::update vertices");
        });
    }
}

void TextMesh::draw()
{
    auto& f = FontManager::get(font, size, weight, italic);
    if (f.font_tex.ready())
    {
        activate_text_texture_unit(0U);
        f.font_tex.bind();
        FontManager::m_sampler.bind(0);

        (void)pp::legacy::gl_mesh::draw_mesh(
            pp::renderer::gl::OpenGlMeshDraw {
                .vertex_array = font_array,
                .mode = pp::renderer::gl::primitive_mode_for_fill_count(3U),
                .count = font_array_count,
                .indexed = true,
                .index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)),
                .index_offset = nullptr,
            },
            "TextMesh::draw");

        f.font_tex.unbind();
        FontManager::m_sampler.unbind();
    }
}
