#include "viz/graph_icons.h"

#include "gfx/gl_loader.h"

// stb_truetype esta vendor-ada por ImGui. La declaracion `STBTT_DEF static`
// hace que cada TU tenga su propia copia de las funciones — no colisionamos
// con el `STB_TRUETYPE_IMPLEMENTATION` que ya esta en `imgui_draw.cpp`.
#define STB_TRUETYPE_IMPLEMENTATION
#include "imstb_truetype.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

#ifndef FN_CPP_ROOT
#define FN_CPP_ROOT ""
#endif

// Hook para tests sin contexto GL. Se setea via la variable de entorno
// `FN_GRAPH_ICONS_SKIP_GL=1` antes de llamar a `graph_icons_build`. Cuando
// esta activo, el atlas se construye en CPU pero `gl_tex` queda en 0 (los
// tests pueden inspeccionar `pixels` y `regions`/`uv_table` sin GL).
namespace {

bool skip_gl_upload() {
    const char* v = std::getenv("FN_GRAPH_ICONS_SKIP_GL");
    return v && v[0] && v[0] != '0';
}

constexpr int k_atlas_w   = 512;
constexpr int k_atlas_h   = 512;
constexpr int k_grid      = 16;     // 16x16 celdas
constexpr int k_max_icons = k_grid * k_grid;

bool file_exists(const char* path) {
    if (!path || !*path) return false;
    if (FILE* f = std::fopen(path, "rb")) { std::fclose(f); return true; }
    return false;
}

// Mismo orden de busqueda que `icon_font.cpp` para que cualquier app del
// registry encuentre el TTF tras el script de copy de assets.
std::string find_tabler_ttf() {
    const char* fname = "tabler-icons.ttf";
    std::string p;
    p = std::string("./") + fname;        if (file_exists(p.c_str())) return p;
    p = std::string("./assets/") + fname; if (file_exists(p.c_str())) return p;
    if (const char* env = std::getenv("FN_ASSETS_DIR")) {
        p = std::string(env) + "/" + fname;
        if (file_exists(p.c_str())) return p;
    }
    if (std::strlen(FN_CPP_ROOT) > 0) {
        p = std::string(FN_CPP_ROOT) + "/vendor/tabler-icons/tabler-icons.ttf";
        if (file_exists(p.c_str())) return p;
    }
    return std::string();
}

std::vector<unsigned char> read_file_bytes(const char* path) {
    std::vector<unsigned char> out;
    FILE* f = std::fopen(path, "rb");
    if (!f) return out;
    std::fseek(f, 0, SEEK_END);
    long sz = std::ftell(f);
    std::fseek(f, 0, SEEK_SET);
    if (sz > 0) {
        out.resize((size_t)sz);
        size_t rd = std::fread(out.data(), 1, (size_t)sz, f);
        if (rd != (size_t)sz) out.clear();
    }
    std::fclose(f);
    return out;
}

} // namespace

struct IconAtlas {
    unsigned int gl_tex = 0;
    int          width  = 0;
    int          height = 0;
    int          icon_px = 32;
    int          count  = 0;
    std::vector<IconRegion>      regions;        // index 0 dummy (id=0 -> nullptr)
    std::vector<unsigned char>   pixels_rgba;    // CPU copy para tests
    std::vector<float>           uv_table;       // count*4 floats, 0-indexed
};

IconAtlas* graph_icons_build(const uint16_t* codepoints, int count, int icon_px) {
    if (!codepoints || count <= 0 || count > k_max_icons) return nullptr;
    if (icon_px <= 0)       icon_px = 32;
    if (icon_px > k_atlas_w / k_grid) icon_px = k_atlas_w / k_grid;

    std::string ttf_path = find_tabler_ttf();
    if (ttf_path.empty()) {
        std::fprintf(stderr,
            "[graph_icons] tabler-icons.ttf no encontrado. Buscado en ./, "
            "./assets/, $FN_ASSETS_DIR, %s/vendor/tabler-icons/\n",
            FN_CPP_ROOT[0] ? FN_CPP_ROOT : "(FN_CPP_ROOT vacio)");
        return nullptr;
    }
    auto ttf_bytes = read_file_bytes(ttf_path.c_str());
    if (ttf_bytes.empty()) {
        std::fprintf(stderr, "[graph_icons] no pude leer %s\n", ttf_path.c_str());
        return nullptr;
    }

    stbtt_fontinfo font;
    if (!stbtt_InitFont(&font, ttf_bytes.data(),
                        stbtt_GetFontOffsetForIndex(ttf_bytes.data(), 0))) {
        std::fprintf(stderr, "[graph_icons] stbtt_InitFont fallo\n");
        return nullptr;
    }

    IconAtlas* a = new IconAtlas();
    a->width   = k_atlas_w;
    a->height  = k_atlas_h;
    a->icon_px = icon_px;
    a->count   = count;
    a->regions.reserve((size_t)count + 1);
    a->regions.push_back({0, 0, 0.f, 0.f, 0.f, 0.f}); // id=0 reservado
    a->pixels_rgba.assign((size_t)k_atlas_w * (size_t)k_atlas_h * 4, 0);
    a->uv_table.assign((size_t)count * 4, 0.f);

    // Padding 1 px dentro de cada celda para que el filtrado linear no muestre
    // pixels del icono vecino al sumar `fwidth`.
    const int cell    = k_atlas_w / k_grid; // 32 px
    const int padding = 1;
    const int target  = cell - 2 * padding; // 30 px dentro de la celda

    const float scale = stbtt_ScaleForPixelHeight(&font, (float)target);

    int ascent = 0, descent = 0, lineGap = 0;
    stbtt_GetFontVMetrics(&font, &ascent, &descent, &lineGap);
    const float baseline = (float)ascent * scale;

    for (int i = 0; i < count; ++i) {
        const uint16_t cp = codepoints[i];
        const int row = i / k_grid;
        const int col = i % k_grid;
        const int cx0 = col * cell;
        const int cy0 = row * cell;

        // Bitmap box en pixels (relativo al baseline).
        int x0, y0, x1, y1;
        stbtt_GetCodepointBitmapBox(&font, cp, scale, scale, &x0, &y0, &x1, &y1);
        const int gw = x1 - x0;
        const int gh = y1 - y0;

        // Alinear glifo dentro de la celda manteniendo el padding y el centrado.
        // Algunos iconos no caben en `target` exacto (Tabler tiene viewBox uniforme
        // pero el bitmap puede salirse 1-2 px). Si gw > target lo recortamos al
        // mismo target — la imagen sale ligeramente comprimida pero no pisa la
        // celda vecina.
        const int draw_w = (gw > target) ? target : gw;
        const int draw_h = (gh > target) ? target : gh;
        const int dx = cx0 + padding + (target - draw_w) / 2;
        const int dy = cy0 + padding + (target - draw_h) / 2;

        if (draw_w > 0 && draw_h > 0) {
            std::vector<unsigned char> mono((size_t)draw_w * (size_t)draw_h, 0);
            stbtt_MakeCodepointBitmap(&font, mono.data(),
                                      draw_w, draw_h, draw_w,
                                      scale, scale, cp);

            // Copia mono → RGBA (R=G=B=255, A=mono).
            for (int yy = 0; yy < draw_h; ++yy) {
                for (int xx = 0; xx < draw_w; ++xx) {
                    const unsigned char alpha = mono[(size_t)yy * draw_w + xx];
                    if (alpha == 0) continue;
                    const size_t off = ((size_t)(dy + yy) * k_atlas_w + (size_t)(dx + xx)) * 4;
                    a->pixels_rgba[off + 0] = 0xFFu;
                    a->pixels_rgba[off + 1] = 0xFFu;
                    a->pixels_rgba[off + 2] = 0xFFu;
                    a->pixels_rgba[off + 3] = alpha;
                }
            }
        }

        // UVs: bordes externos de la celda (padding fuera de los UVs para que el
        // shader pueda hacer un overlay perfectamente cuadrado sin bleed).
        IconRegion r{};
        r.id        = (uint16_t)(i + 1);
        r.codepoint = cp;
        r.u0 = (float)(cx0 + padding) / (float)k_atlas_w;
        r.v0 = (float)(cy0 + padding) / (float)k_atlas_h;
        r.u1 = (float)(cx0 + cell - padding) / (float)k_atlas_w;
        r.v1 = (float)(cy0 + cell - padding) / (float)k_atlas_h;
        a->regions.push_back(r);
        a->uv_table[(size_t)i * 4 + 0] = r.u0;
        a->uv_table[(size_t)i * 4 + 1] = r.v0;
        a->uv_table[(size_t)i * 4 + 2] = r.u1;
        a->uv_table[(size_t)i * 4 + 3] = r.v1;
        (void)baseline; // baseline no se usa en este layout (cada celda absorbe el offset)
    }

    if (skip_gl_upload()) {
        return a;
    }

    // Subir a GPU.
    glGenTextures(1, &a->gl_tex);
    glBindTexture(GL_TEXTURE_2D, a->gl_tex);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, a->width, a->height, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, a->pixels_rgba.data());
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_2D, 0);

    return a;
}

unsigned int graph_icons_texture(const IconAtlas* a) {
    return a ? a->gl_tex : 0u;
}

const IconRegion* graph_icons_region(const IconAtlas* a, uint16_t icon_id) {
    if (!a || icon_id == 0) return nullptr;
    if (icon_id >= (uint16_t)a->regions.size()) return nullptr;
    return &a->regions[icon_id];
}

int graph_icons_count(const IconAtlas* a)  { return a ? a->count    : 0; }
int graph_icons_width(const IconAtlas* a)  { return a ? a->width    : 0; }
int graph_icons_height(const IconAtlas* a) { return a ? a->height   : 0; }
const unsigned char* graph_icons_pixels(const IconAtlas* a) {
    return a ? a->pixels_rgba.data() : nullptr;
}
const float* graph_icons_uv_table(const IconAtlas* a) {
    return a ? a->uv_table.data() : nullptr;
}

void graph_icons_destroy(IconAtlas* a) {
    if (!a) return;
    if (a->gl_tex) glDeleteTextures(1, &a->gl_tex);
    delete a;
}
