#include "viz/graph_renderer.h"
#include "viz/graph_types.h"

// gl_loader: en Linux es no-op (incluye GL headers con GL_GLEXT_PROTOTYPES);
// en Windows expone los punteros via #define gl* fn_gl* tras gl_loader_init().
#include "gfx/gl_loader.h"

#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cstddef>
#include <cmath>
#include <algorithm>

// ---------------------------------------------------------------------------
// Fallback palette (RGBA8 con R en LSB) — usada solo cuando GraphData::types
// esta vacio. En el modelo extendido (issue 0049e) la apariencia de cada
// nodo viene resuelta por `resolve_node_color()`, que mira primero el
// override del nodo, luego el EntityType de la tabla, y finalmente este
// fallback. Mantener 10 colores para nodos sin tipos sigue siendo util en
// demos que aun no construyen tablas EntityType.
// ---------------------------------------------------------------------------
static const uint32_t k_fallback_palette[10] = {
    0xFF4CAF50u, 0xFFF44336u, 0xFF2196F3u, 0xFFFF9800u, 0xFF9C27B0u,
    0xFF00BCD4u, 0xFFFFEB3Bu, 0xFFE91E63u, 0xFF795548u, 0xFF607D8Bu,
};

// Maximo de iconos que cabe en el uniform array del shader. 256 es lo que
// genera `graph_icons` (grid 16×16 en 512×512). Subirlo requiere mas budget
// de uniforms (vec4×N → 4 floats por entrada) y aun cabe holgado en el
// limite GL 3.30 de 1024 vec4 por bloque.
static constexpr int k_max_icons = 256;

// ---------------------------------------------------------------------------
// Per-instance / per-vertex data layouts
// ---------------------------------------------------------------------------
// 0049f: NodeInstance crece de 16 a 24 bytes para llevar shape + icon_id.
// `shape_icon` empaqueta shape (8 bits bajos) + icon_id (16 bits siguientes);
// los UVs del icono no viajan por instancia — el shader los busca en un
// `uniform vec4 u_icon_uvs[256]` indexado por icon_id-1. Asi conservamos
// bandwidth aunque haya muchos nodos con el mismo icono.
struct NodeInstance {     // 24 bytes (alineado a 4)
    float    x, y;        // 8
    float    size;        // 4 (= diametro en pixels world-space)
    uint32_t color;       // 4
    uint32_t shape_icon;  // 4 — (shape & 0xFF) | (icon_id << 8)
    uint32_t pad_;        // 4 — relleno explicito; reservado para flags futuros
};

// 0049f: EdgeStatic crece de 16 a 20 bytes para llevar style + flags reales.
// `style_flags`: flags (low 8 bits) | style (next 8 bits). El resto sigue
// siendo source/target/color como en 0049d.
struct EdgeStatic {        // 20 bytes
    uint32_t source;       // index into nodes
    uint32_t target;       // index into nodes
    uint32_t color;        // packed RGBA8
    uint32_t style_flags;  // (flags & 0xFF) | (style << 8)
    uint32_t pad_;         // pad a multiplo de 4 — actualmente sin uso
};

// ---------------------------------------------------------------------------
// Internal struct
// ---------------------------------------------------------------------------
struct GraphRenderer {
    unsigned int fbo;
    unsigned int texture;
    unsigned int rbo;
    int width, height;

    // Node rendering (instanced quads)
    unsigned int node_vao, node_quad_vbo, node_instance_vbo;
    unsigned int node_shader;
    int          node_u_viewport_loc;
    int          node_u_scale_loc;
    int          node_u_translate_loc;
    int          node_u_outline_loc;
    int          node_u_node_px_loc;
    int          node_u_icon_atlas_loc;
    int          node_u_has_icons_loc;
    int          node_u_icon_uvs_loc;

    // Edge rendering (vertex pulling con 6 vertices/instancia para flecha)
    unsigned int edge_vao, edge_vbo;
    unsigned int edge_shader;
    unsigned int node_pos_buf;
    unsigned int node_pos_tex;
    int          edge_u_viewport_loc;
    int          edge_u_scale_loc;
    int          edge_u_translate_loc;
    int          edge_u_alpha_loc;
    int          edge_u_node_pos_loc;

    // Streaming buffer capacities (in bytes).
    size_t node_vbo_capacity;
    size_t node_pos_capacity;
    size_t edge_static_capacity;

    // CPU staging
    NodeInstance* node_staging;
    size_t        node_staging_cap;
    float*        node_pos_staging;
    size_t        node_pos_staging_cap;
    EdgeStatic*   edge_static_staging;
    size_t        edge_static_staging_cap;

    // Edge cache (reupload solo cuando cambia el grafo)
    const void* cached_edges_ptr;
    int         cached_edge_count;
    int         cached_edges_drawn;
    bool        edges_uploaded;

    // Icon atlas binding (0 = sin iconos)
    unsigned int icon_atlas_tex;
    float        icon_uvs[k_max_icons * 4];
    int          icon_uv_count;

    GraphRendererConfig config;
};

// ---------------------------------------------------------------------------
// Shader sources
// ---------------------------------------------------------------------------

// Node vertex shader — instanced unit quad, ahora con shape + icon_id.
// Pasamos el texto del shape al fragment para que despache el SDF correcto;
// los UVs del icono se buscan en `u_icon_uvs[icon_id-1]`.
static const char* k_node_vert = R"(
#version 330 core
layout(location = 0) in vec2 a_quad;

layout(location = 1) in vec2  a_pos;
layout(location = 2) in float a_size;
layout(location = 3) in uint  a_color;
layout(location = 4) in uint  a_shape_icon;

out vec2 v_uv;
out vec4 v_color;
flat out uint v_shape;
flat out uint v_icon_id;
flat out vec4 v_icon_uv;

uniform vec2  u_viewport;
uniform float u_scale;
uniform vec2  u_translate;
uniform vec4  u_icon_uvs[256];

vec4 unpack_rgba8(uint c) {
    return vec4(
        float( c        & 0xFFu),
        float((c >>  8) & 0xFFu),
        float((c >> 16) & 0xFFu),
        float((c >> 24) & 0xFFu)
    ) * (1.0 / 255.0);
}

void main() {
    vec2 screen = a_pos * u_scale + u_translate;
    screen += a_quad * a_size * u_scale;
    vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
    ndc.y = -ndc.y;
    gl_Position = vec4(ndc, 0.0, 1.0);
    v_uv      = a_quad + 0.5;
    v_color   = unpack_rgba8(a_color);
    v_shape   = a_shape_icon & 0xFFu;
    v_icon_id = (a_shape_icon >> 8) & 0xFFFFu;
    if (v_icon_id != 0u) {
        v_icon_uv = u_icon_uvs[int(v_icon_id) - 1];
    } else {
        v_icon_uv = vec4(0.0);
    }
}
)";

// Node fragment shader — SDF dispatch + opcional icon overlay.
// Para mantener la calidad del AA usamos `fwidth(d)` en lugar del
// `1.5/u_node_px` viejo: sirve igual a cualquier zoom y se queda nitido en
// los bordes complejos (hexagono, triangulo).
static const char* k_node_frag = R"(
#version 330 core
in vec2 v_uv;
in vec4 v_color;
flat in uint v_shape;
flat in uint v_icon_id;
flat in vec4 v_icon_uv;

out vec4 frag_color;

uniform float u_outline_px;
uniform float u_node_px;
uniform sampler2D u_icon_atlas;
uniform int u_has_icons;

float sdf_circle(vec2 uv) {
    return length(uv - 0.5) - 0.5;
}

float sdf_square(vec2 uv) {
    vec2 d = abs(uv - 0.5) - 0.5;
    return max(d.x, d.y);
}

float sdf_diamond(vec2 uv) {
    vec2 d = abs(uv - 0.5);
    return d.x + d.y - 0.5;
}

// Hexagono regular alineado horizontalmente; SDF derivado del clasico de
// Inigo Quilez adaptado al cuadrado [0,1]^2. Inscribimos el hex dentro del
// circulo de radio 0.5 para que sus vertices toquen los bordes — asi
// area visual ~ a la del circulo del mismo `size`.
float sdf_hex(vec2 uv) {
    vec2 p = abs(uv - 0.5);
    const vec2 k = vec2(0.866025404, 0.5);
    p -= 2.0 * min(dot(k, p), 0.0) * k;
    p -= vec2(clamp(p.x, -k.y * 0.5, k.y * 0.5), 0.5);
    return length(p) * sign(p.y) - (0.5 * 0.866025404);
}

// Triangulo equilatero apuntando hacia arriba dentro de [0,1]^2.
float sdf_triangle(vec2 uv) {
    const float k = 1.732050808; // sqrt(3)
    vec2 p = uv - vec2(0.5, 0.5);
    p.x = abs(p.x) - 0.5;
    p.y = p.y + 0.5 / k;
    if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
    p.x -= clamp(p.x, -1.0, 0.0);
    return -length(p) * sign(p.y);
}

float sdf_rrect(vec2 uv) {
    float r = 0.18;
    vec2 d = abs(uv - 0.5) - (0.5 - r);
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r;
}

float pick_sdf(uint shape, vec2 uv) {
    // shape 0 = SHAPE_USE_TYPE: el CPU resuelve antes; aqui no debe llegar.
    // 1=circle 2=square 3=diamond 4=hex 5=triangle 6=rounded_square
    if      (shape == 1u) return sdf_circle(uv);
    else if (shape == 2u) return sdf_square(uv);
    else if (shape == 3u) return sdf_diamond(uv);
    else if (shape == 4u) return sdf_hex(uv);
    else if (shape == 5u) return sdf_triangle(uv);
    else if (shape == 6u) return sdf_rrect(uv);
    return sdf_circle(uv); // default robusto si shape mal codificado
}

void main() {
    float d  = pick_sdf(v_shape, v_uv);
    float aa = max(fwidth(d), 0.001);
    float fill_alpha = 1.0 - smoothstep(-aa, 0.0, d);
    if (fill_alpha < 0.001) discard;

    // Outline: anillo exterior — la anchura en uv viene de outline_px / node_px.
    float outline_uv = u_outline_px / max(u_node_px, 1.0);
    float outline    = smoothstep(-outline_uv - aa, -outline_uv, d);
    vec3 fill        = v_color.rgb;
    vec3 outline_col = mix(fill, vec3(1.0), 0.6);
    vec3 col         = mix(fill, outline_col, outline);

    // Overlay del icono (solo si hay atlas + icon_id != 0). El icono se
    // tintamos sumando blanco modulado por su alpha — el resultado sigue
    // siendo legible sobre cualquier color de fondo del nodo.
    if (u_has_icons != 0 && v_icon_id != 0u) {
        vec2 atlas_uv = mix(v_icon_uv.xy, v_icon_uv.zw, v_uv);
        vec4 ic = texture(u_icon_atlas, atlas_uv);
        col = mix(col, vec3(1.0), ic.a * 0.85);
    }

    frag_color = vec4(col, v_color.a * fill_alpha);
}
)";

// Edge vertex shader — vertex pulling, ahora 6 vertices por arista para
// soportar flecha en aristas EF_DIRECTED:
//   gl_VertexID 0 (line src→tip), 1 (tip)
//   gl_VertexID 2 (tip), 3 (back_left)
//   gl_VertexID 4 (tip), 5 (back_right)
// Si no esta directed, los vertices 2..5 se colapsan al tip (lineas
// degeneradas no visibles).
//
// Para dashed/dotted: pasamos `arc_length` interpolado (en pixels) al
// fragment shader; este descarta segun style.
static const char* k_edge_vert = R"(
#version 330 core
layout(location = 0) in uint a_source;
layout(location = 1) in uint a_target;
layout(location = 2) in uint a_color;
layout(location = 3) in uint a_style_flags;

uniform samplerBuffer u_node_pos;
uniform vec2  u_viewport;
uniform float u_scale;
uniform vec2  u_translate;
uniform float u_alpha;

out vec4 v_color;
flat out uint v_style;
flat out uint v_segment; // 0=line, 1=arrow
out float v_arc;

vec4 unpack_rgba8(uint c) {
    return vec4(
        float( c        & 0xFFu),
        float((c >>  8) & 0xFFu),
        float((c >> 16) & 0xFFu),
        float((c >> 24) & 0xFFu)
    ) * (1.0 / 255.0);
}

vec2 to_screen(vec2 wpos) {
    return wpos * u_scale + u_translate;
}

vec2 to_ndc(vec2 screen) {
    vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
    return vec2(ndc.x, -ndc.y);
}

void main() {
    int  vid     = gl_VertexID;
    uint flags   = a_style_flags & 0xFFu;
    uint style   = (a_style_flags >> 8) & 0xFFu;
    bool directed = (flags & 1u) != 0u; // EF_DIRECTED == 1

    vec2 wsrc = texelFetch(u_node_pos, int(a_source)).xy;
    vec2 wtgt = texelFetch(u_node_pos, int(a_target)).xy;

    vec2 ssrc = to_screen(wsrc);
    vec2 stgt = to_screen(wtgt);

    vec2 dir  = stgt - ssrc;
    float seg_len = length(dir);
    vec2 dir_n = (seg_len > 0.0001) ? dir / seg_len : vec2(1.0, 0.0);
    vec2 perp  = vec2(-dir_n.y, dir_n.x);

    // Acortamos el segmento principal si la arista es directed para que la
    // flecha no se incruste en el nodo target. Tamano fijo en pixels = 10.
    float arrow_px = 10.0;
    vec2 tip = stgt;
    vec2 line_end = directed ? (stgt - dir_n * arrow_px * 0.5) : stgt;

    vec2 spos;
    float arc;
    uint segment;

    if (vid <= 1) {
        // Linea principal source→line_end.
        segment = 0u;
        if (vid == 0) { spos = ssrc;     arc = 0.0; }
        else          { spos = line_end; arc = length(line_end - ssrc); }
    } else {
        segment = 1u;
        arc = 0.0;
        if (!directed) {
            // Sin flecha: degenerado en el tip — sin pintar.
            spos = tip;
        } else {
            // Triangulo de la flecha en 2 lineas (chevron):
            //   (tip, back_left) y (tip, back_right)
            vec2 back     = tip - dir_n * arrow_px;
            vec2 left_p   = back + perp * arrow_px * 0.5;
            vec2 right_p  = back - perp * arrow_px * 0.5;
            if      (vid == 2) spos = tip;
            else if (vid == 3) spos = left_p;
            else if (vid == 4) spos = tip;
            else               spos = right_p;
        }
    }

    gl_Position = vec4(to_ndc(spos), 0.0, 1.0);

    vec4 c = unpack_rgba8(a_color);
    c.a *= u_alpha;
    v_color   = c;
    v_style   = style;
    v_segment = segment;
    v_arc     = arc;
}
)";

// Edge fragment shader — descarta segun style + arc_length para producir
// dashed (period 8 px, duty 0.5) o dotted (period 4 px, duty 0.25). Las
// lineas de la flecha (segment==1) se renderizan siempre solidas.
static const char* k_edge_frag = R"(
#version 330 core
in vec4 v_color;
flat in uint v_style;
flat in uint v_segment;
in float v_arc;
out vec4 frag_color;

void main() {
    if (v_segment == 0u) {
        // EDGE_USE_TYPE=0, EDGE_SOLID=1, EDGE_DASHED=2, EDGE_DOTTED=3
        if (v_style == 2u) {
            if (mod(v_arc, 8.0) > 4.0) discard;
        } else if (v_style == 3u) {
            if (mod(v_arc, 4.0) > 1.0) discard;
        }
    }
    frag_color = v_color;
}
)";

// ---------------------------------------------------------------------------
// Shader helpers
// ---------------------------------------------------------------------------
static unsigned int compile_shader(GLenum type, const char* src) {
    unsigned int s = glCreateShader(type);
    glShaderSource(s, 1, &src, nullptr);
    glCompileShader(s);
    int ok;
    glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char buf[1024];
        glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
        fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
    }
    return s;
}

static unsigned int link_program(const char* vert_src, const char* frag_src) {
    unsigned int vs = compile_shader(GL_VERTEX_SHADER,   vert_src);
    unsigned int fs = compile_shader(GL_FRAGMENT_SHADER, frag_src);
    unsigned int prog = glCreateProgram();
    glAttachShader(prog, vs);
    glAttachShader(prog, fs);
    glLinkProgram(prog);
    int ok;
    glGetProgramiv(prog, GL_LINK_STATUS, &ok);
    if (!ok) {
        char buf[1024];
        glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
        fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
    }
    glDeleteShader(vs);
    glDeleteShader(fs);
    return prog;
}

// ---------------------------------------------------------------------------
// FBO helpers
// ---------------------------------------------------------------------------
static void create_fbo(GraphRenderer* r) {
    glGenTextures(1, &r->texture);
    glBindTexture(GL_TEXTURE_2D, r->texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, r->width, r->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    glGenRenderbuffers(1, &r->rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, r->rbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, r->width, r->height);
    glBindRenderbuffer(GL_RENDERBUFFER, 0);

    glGenFramebuffers(1, &r->fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, r->texture, 0);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, r->rbo);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

static void destroy_fbo(GraphRenderer* r) {
    glDeleteFramebuffers(1,  &r->fbo);
    glDeleteTextures(1,      &r->texture);
    glDeleteRenderbuffers(1, &r->rbo);
    r->fbo = r->texture = r->rbo = 0;
}

// ---------------------------------------------------------------------------
// Capacity-tracked streaming helpers
// ---------------------------------------------------------------------------
static size_t grow_capacity(size_t current, size_t needed, size_t initial) {
    size_t cap = current > 0 ? current : initial;
    while (cap < needed) cap *= 2;
    return cap;
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config) {
    GraphRenderer* r = new GraphRenderer();
    r->width  = width;
    r->height = height;
    r->config = config;

    r->node_vbo_capacity        = 0;
    r->node_pos_capacity        = 0;
    r->edge_static_capacity     = 0;
    r->node_staging             = nullptr;
    r->node_staging_cap         = 0;
    r->node_pos_staging         = nullptr;
    r->node_pos_staging_cap     = 0;
    r->edge_static_staging      = nullptr;
    r->edge_static_staging_cap  = 0;
    r->cached_edges_ptr         = nullptr;
    r->cached_edge_count        = 0;
    r->cached_edges_drawn       = 0;
    r->edges_uploaded           = false;
    r->icon_atlas_tex           = 0;
    r->icon_uv_count            = 0;
    std::memset(r->icon_uvs, 0, sizeof(r->icon_uvs));

    create_fbo(r);

    static const float quad_verts[8] = {
        -0.5f, -0.5f,
         0.5f, -0.5f,
        -0.5f,  0.5f,
         0.5f,  0.5f,
    };

    glGenVertexArrays(1, &r->node_vao);
    glBindVertexArray(r->node_vao);

    glGenBuffers(1, &r->node_quad_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);

    glGenBuffers(1, &r->node_instance_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(NodeInstance),
                          (void*)offsetof(NodeInstance, x));
    glVertexAttribDivisor(1, 1);
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(NodeInstance),
                          (void*)offsetof(NodeInstance, size));
    glVertexAttribDivisor(2, 1);
    glEnableVertexAttribArray(3);
    glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, sizeof(NodeInstance),
                           (void*)offsetof(NodeInstance, color));
    glVertexAttribDivisor(3, 1);
    glEnableVertexAttribArray(4);
    glVertexAttribIPointer(4, 1, GL_UNSIGNED_INT, sizeof(NodeInstance),
                           (void*)offsetof(NodeInstance, shape_icon));
    glVertexAttribDivisor(4, 1);

    glBindVertexArray(0);

    glGenVertexArrays(1, &r->edge_vao);
    glBindVertexArray(r->edge_vao);

    glGenBuffers(1, &r->edge_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);

    glEnableVertexAttribArray(0);
    glVertexAttribIPointer(0, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
                           (void*)offsetof(EdgeStatic, source));
    glVertexAttribDivisor(0, 1);
    glEnableVertexAttribArray(1);
    glVertexAttribIPointer(1, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
                           (void*)offsetof(EdgeStatic, target));
    glVertexAttribDivisor(1, 1);
    glEnableVertexAttribArray(2);
    glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
                           (void*)offsetof(EdgeStatic, color));
    glVertexAttribDivisor(2, 1);
    glEnableVertexAttribArray(3);
    glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
                           (void*)offsetof(EdgeStatic, style_flags));
    glVertexAttribDivisor(3, 1);

    glBindVertexArray(0);

    glGenBuffers(1, &r->node_pos_buf);
    glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
    glBufferData(GL_TEXTURE_BUFFER, 4096 * 2 * sizeof(float), nullptr, GL_STREAM_DRAW);
    r->node_pos_capacity = 4096 * 2 * sizeof(float);

    glGenTextures(1, &r->node_pos_tex);
    glBindTexture(GL_TEXTURE_BUFFER, r->node_pos_tex);
    glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, r->node_pos_buf);
    glBindTexture(GL_TEXTURE_BUFFER, 0);
    glBindBuffer(GL_TEXTURE_BUFFER, 0);

    r->node_shader = link_program(k_node_vert, k_node_frag);
    r->edge_shader = link_program(k_edge_vert, k_edge_frag);

    r->node_u_viewport_loc   = glGetUniformLocation(r->node_shader, "u_viewport");
    r->node_u_scale_loc      = glGetUniformLocation(r->node_shader, "u_scale");
    r->node_u_translate_loc  = glGetUniformLocation(r->node_shader, "u_translate");
    r->node_u_outline_loc    = glGetUniformLocation(r->node_shader, "u_outline_px");
    r->node_u_node_px_loc    = glGetUniformLocation(r->node_shader, "u_node_px");
    r->node_u_icon_atlas_loc = glGetUniformLocation(r->node_shader, "u_icon_atlas");
    r->node_u_has_icons_loc  = glGetUniformLocation(r->node_shader, "u_has_icons");
    r->node_u_icon_uvs_loc   = glGetUniformLocation(r->node_shader, "u_icon_uvs");

    r->edge_u_viewport_loc  = glGetUniformLocation(r->edge_shader, "u_viewport");
    r->edge_u_scale_loc     = glGetUniformLocation(r->edge_shader, "u_scale");
    r->edge_u_translate_loc = glGetUniformLocation(r->edge_shader, "u_translate");
    r->edge_u_alpha_loc     = glGetUniformLocation(r->edge_shader, "u_alpha");
    r->edge_u_node_pos_loc  = glGetUniformLocation(r->edge_shader, "u_node_pos");

    return r;
}

void graph_renderer_destroy(GraphRenderer* r) {
    if (!r) return;
    destroy_fbo(r);
    glDeleteVertexArrays(1, &r->node_vao);
    glDeleteBuffers(1, &r->node_quad_vbo);
    glDeleteBuffers(1, &r->node_instance_vbo);
    glDeleteVertexArrays(1, &r->edge_vao);
    glDeleteBuffers(1, &r->edge_vbo);
    glDeleteBuffers(1, &r->node_pos_buf);
    glDeleteTextures(1, &r->node_pos_tex);
    glDeleteProgram(r->node_shader);
    glDeleteProgram(r->edge_shader);
    free(r->node_staging);
    free(r->node_pos_staging);
    free(r->edge_static_staging);
    delete r;
}

void graph_renderer_resize(GraphRenderer* r, int width, int height) {
    if (!r) return;
    if (r->width == width && r->height == height) return;
    r->width  = width;
    r->height = height;
    destroy_fbo(r);
    create_fbo(r);
}

void graph_renderer_set_icon_atlas(GraphRenderer* r,
                                    unsigned int texture_id,
                                    const float* uv_table,
                                    int count) {
    if (!r) return;
    r->icon_atlas_tex = texture_id;
    r->icon_uv_count  = (count > k_max_icons) ? k_max_icons : (count < 0 ? 0 : count);
    if (r->icon_uv_count > 0 && uv_table) {
        std::memcpy(r->icon_uvs, uv_table,
                    (size_t)r->icon_uv_count * 4 * sizeof(float));
    }
    // Limpia las entradas no usadas para evitar UVs basura si el shader las
    // sample por error. Costo: O(k_max_icons) — irrelevante.
    if (r->icon_uv_count < k_max_icons) {
        std::memset(r->icon_uvs + r->icon_uv_count * 4, 0,
                    (size_t)(k_max_icons - r->icon_uv_count) * 4 * sizeof(float));
    }
}

unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
                                  float cam_x, float cam_y, float cam_zoom) {
    if (!r) return 0;

    GLint prev_fbo;
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
    GLint prev_viewport[4];
    glGetIntegerv(GL_VIEWPORT, prev_viewport);

    glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
    glViewport(0, 0, r->width, r->height);

    uint8_t br, bg, bb, ba;
    unpack_rgba8(r->config.bg_color, br, bg, bb, ba);
    glClearColor(br / 255.0f, bg / 255.0f, bb / 255.0f, ba / 255.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    float scale = cam_zoom;
    float tx = -cam_x * scale + (float)r->width  * 0.5f;
    float ty = -cam_y * scale + (float)r->height * 0.5f;

    float half_w = ((float)r->width  * 0.5f) / std::max(scale, 0.0001f);
    float half_h = ((float)r->height * 0.5f) / std::max(scale, 0.0001f);
    const float margin = 0.10f;
    float vx0 = cam_x - half_w * (1.0f + margin);
    float vx1 = cam_x + half_w * (1.0f + margin);
    float vy0 = cam_y - half_h * (1.0f + margin);
    float vy1 = cam_y + half_h * (1.0f + margin);

    // ----------------------------------------------------------------
    // Subir posiciones de nodos al TBO.
    // ----------------------------------------------------------------
    bool tbo_ready = false;
    if (graph.node_count > 0 && graph.nodes) {
        size_t need_floats = (size_t)graph.node_count * 2;
        if (need_floats > r->node_pos_staging_cap) {
            size_t new_cap = grow_capacity(r->node_pos_staging_cap, need_floats, 8192);
            r->node_pos_staging = (float*)realloc(r->node_pos_staging, new_cap * sizeof(float));
            r->node_pos_staging_cap = new_cap;
        }
        for (int i = 0; i < graph.node_count; ++i) {
            r->node_pos_staging[i * 2 + 0] = graph.nodes[i].x;
            r->node_pos_staging[i * 2 + 1] = graph.nodes[i].y;
        }
        const size_t used_bytes = need_floats * sizeof(float);
        if (used_bytes > r->node_pos_capacity) {
            r->node_pos_capacity = grow_capacity(r->node_pos_capacity, used_bytes,
                                                 4096 * 2 * sizeof(float));
        }
        glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
        glBufferData(GL_TEXTURE_BUFFER, (GLsizeiptr)r->node_pos_capacity, nullptr, GL_STREAM_DRAW);
        glBufferSubData(GL_TEXTURE_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_pos_staging);
        glBindBuffer(GL_TEXTURE_BUFFER, 0);
        tbo_ready = true;
    }

    // ----------------------------------------------------------------
    // Aristas via vertex pulling. 6 vertices por arista (line + arrow).
    // El buffer estatico se reupload solo cuando cambia el grafo.
    // ----------------------------------------------------------------
    if (tbo_ready && graph.edge_count > 0 && graph.edges) {
        const bool graph_changed =
            !r->edges_uploaded
            || r->cached_edges_ptr  != (const void*)graph.edges
            || r->cached_edge_count != graph.edge_count;

        if (graph_changed) {
            if ((size_t)graph.edge_count > r->edge_static_staging_cap) {
                size_t new_cap = grow_capacity(r->edge_static_staging_cap,
                                               (size_t)graph.edge_count, 8192);
                r->edge_static_staging = (EdgeStatic*)realloc(r->edge_static_staging,
                                                              new_cap * sizeof(EdgeStatic));
                r->edge_static_staging_cap = new_cap;
            }
            size_t out = 0;
            for (int i = 0; i < graph.edge_count; ++i) {
                const GraphEdge& e = graph.edges[i];
                if (e.source >= (uint32_t)graph.node_count) continue;
                if (e.target >= (uint32_t)graph.node_count) continue;
                if (!(e.flags & EF_VISIBLE)) continue;
                if (!(graph.nodes[e.source].flags & NF_VISIBLE)) continue;
                if (!(graph.nodes[e.target].flags & NF_VISIBLE)) continue;

                uint32_t col = resolve_edge_color(e, graph.rel_types,
                                                     graph.rel_type_count);
                if (col == 0u) col = pack_rgba8(0x88, 0x88, 0x88, 0xFF);
                uint8_t style = resolve_edge_style(e, graph.rel_types,
                                                      graph.rel_type_count);
                uint32_t style_flags = ((uint32_t)e.flags & 0xFFu)
                                     | ((uint32_t)style  << 8);
                r->edge_static_staging[out++] = { e.source, e.target, col, style_flags, 0u };
            }
            if (out > 0) {
                const size_t used_bytes = out * sizeof(EdgeStatic);
                if (used_bytes > r->edge_static_capacity) {
                    r->edge_static_capacity = grow_capacity(r->edge_static_capacity,
                                                            used_bytes,
                                                            8192 * sizeof(EdgeStatic));
                }
                glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
                glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)r->edge_static_capacity,
                             nullptr, GL_STATIC_DRAW);
                glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)used_bytes,
                                r->edge_static_staging);
            }
            r->cached_edges_ptr   = (const void*)graph.edges;
            r->cached_edge_count  = graph.edge_count;
            r->cached_edges_drawn = (int)out;
            r->edges_uploaded     = (out > 0);
        }

        if (r->edges_uploaded) {
            glUseProgram(r->edge_shader);
            glUniform2f(r->edge_u_viewport_loc, (float)r->width, (float)r->height);
            glUniform1f(r->edge_u_scale_loc,    scale);
            glUniform2f(r->edge_u_translate_loc, tx, ty);
            glUniform1f(r->edge_u_alpha_loc,    r->config.edge_alpha);

            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_BUFFER, r->node_pos_tex);
            glUniform1i(r->edge_u_node_pos_loc, 0);

            glLineWidth(r->config.edge_width);

            glBindVertexArray(r->edge_vao);
            // 6 vertices por instancia: 2 linea + 4 chevron de la flecha.
            glDrawArraysInstanced(GL_LINES, 0, 6, (GLsizei)r->cached_edges_drawn);
            glBindVertexArray(0);
            glBindTexture(GL_TEXTURE_BUFFER, 0);
        }
    } else if (graph.edge_count == 0) {
        r->edges_uploaded = false;
    }

    // ----------------------------------------------------------------
    // Draw nodes (instanced quads, frustum-culled). Empaqueta shape e
    // icon_id por instancia; el shader despacha el SDF y aplica overlay.
    // ----------------------------------------------------------------
    if (graph.node_count > 0 && graph.nodes) {
        if ((size_t)graph.node_count > r->node_staging_cap) {
            size_t new_cap = grow_capacity(r->node_staging_cap, (size_t)graph.node_count, 4096);
            r->node_staging = (NodeInstance*)realloc(r->node_staging, new_cap * sizeof(NodeInstance));
            r->node_staging_cap = new_cap;
        }

        size_t visible = 0;
        for (int i = 0; i < graph.node_count; ++i) {
            const GraphNode& n = graph.nodes[i];
            if (!(n.flags & NF_VISIBLE)) continue;

            float sz = resolve_node_size(n, graph.types, graph.type_count);
            if (sz <= 0.0f) sz = 4.0f;
            float half = sz * 0.5f;
            if (n.x + half < vx0 || n.x - half > vx1) continue;
            if (n.y + half < vy0 || n.y - half > vy1) continue;

            uint32_t ncol;
            if (n.color_override != 0u) {
                ncol = n.color_override;
            } else if (graph.types && n.type_id < (uint16_t)graph.type_count) {
                ncol = graph.types[n.type_id].color;
            } else {
                ncol = k_fallback_palette[n.type_id % 10];
            }

            uint8_t shape = resolve_node_shape(n, graph.types, graph.type_count);
            if (shape == SHAPE_USE_TYPE) shape = SHAPE_CIRCLE;

            // icon_id solo viene del EntityType (los nodos no tienen override
            // de icono en el modelo actual). 0 = sin overlay.
            uint16_t icon_id = 0;
            if (graph.types && n.type_id < (uint16_t)graph.type_count) {
                icon_id = graph.types[n.type_id].icon_id;
            }
            if (icon_id > r->icon_uv_count) icon_id = 0; // fuera de tabla

            uint32_t shape_icon = ((uint32_t)shape & 0xFFu)
                                | ((uint32_t)icon_id << 8);

            r->node_staging[visible++] = { n.x, n.y, sz, ncol, shape_icon, 0u };
        }

        if (visible > 0) {
            const size_t used_bytes = visible * sizeof(NodeInstance);
            if (used_bytes > r->node_vbo_capacity) {
                r->node_vbo_capacity = grow_capacity(r->node_vbo_capacity, used_bytes,
                                                     4096 * sizeof(NodeInstance));
            }

            glUseProgram(r->node_shader);
            glUniform2f(r->node_u_viewport_loc,  (float)r->width, (float)r->height);
            glUniform1f(r->node_u_scale_loc,     scale);
            glUniform2f(r->node_u_translate_loc, tx, ty);
            glUniform1f(r->node_u_outline_loc,   r->config.node_outline);

            float avg_px = 8.0f * scale;
            glUniform1f(r->node_u_node_px_loc, avg_px);

            // Subimos siempre la tabla de UVs — son 256 vec4 = 4KB, peanuts.
            glUniform4fv(r->node_u_icon_uvs_loc, k_max_icons, r->icon_uvs);

            const int has_icons = (r->icon_atlas_tex != 0 && r->icon_uv_count > 0) ? 1 : 0;
            glUniform1i(r->node_u_has_icons_loc, has_icons);
            if (has_icons) {
                glActiveTexture(GL_TEXTURE1);
                glBindTexture(GL_TEXTURE_2D, r->icon_atlas_tex);
                glUniform1i(r->node_u_icon_atlas_loc, 1);
            }

            glBindVertexArray(r->node_vao);
            glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
            glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)r->node_vbo_capacity, nullptr, GL_STREAM_DRAW);
            glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_staging);

            glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)visible);
            glBindVertexArray(0);

            if (has_icons) {
                glActiveTexture(GL_TEXTURE1);
                glBindTexture(GL_TEXTURE_2D, 0);
                glActiveTexture(GL_TEXTURE0);
            }
        }
    }

    glDisable(GL_BLEND);
    glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
    glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);

    return r->texture;
}
