#include "viz/graph_force_layout_gpu.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_types.h"
#include "gfx/gl_loader.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>

// Spatial hash: cada celda guarda hasta K indices de nodos. Si una celda
// satura por encima de K los excedentes se ignoran — el contador atomico
// sigue creciendo pero el shader chequea slot<K antes de escribir. El error
// se manifiesta como repulsion subestimada en zonas muy densas; ajustar
// `grid_cells_per_side` al alza es la solucion.
constexpr int K_MAX_NODES_PER_CELL = 32;

// Bandera global. Se enciende tras un _create exitoso, se apaga si la
// compilacion falla. El demos toggle la consulta para deshabilitar el switch
// CPU/GPU en hardware sin compute.
static bool g_gpu_available = false;

bool graph_force_layout_gpu_available() { return g_gpu_available; }

// ---------------------------------------------------------------------------
// Compute shader sources (#version 430 core)
// ---------------------------------------------------------------------------

// Bindings (std430):
//   0  positions   vec2[N]
//   1  velocities  vec2[N]
//   2  forces      uint[2N]      // uint pairs, bit-casted floats (atomic CAS)
//   3  flags       uint[N]
//   4  edges       uvec2[E]
//   5  weights     float[E]
//   6  grid_counts uint[G²]
//   7  grid_cells  uint[G²*K]    // K = K_MAX_NODES_PER_CELL
//   8  energy      uint[1]       // bit-casted float, atomic CAS

// Helper GLSL: atomicAdd float via CAS loop. Portable en GL 4.3 sin
// extensiones vendor-specific.
static const char* k_glsl_atomic_add_float =
    "void atomic_add_float(uint idx, float value) {\n"
    "    uint cur = forces[idx];\n"
    "    uint expected;\n"
    "    do {\n"
    "        expected = cur;\n"
    "        uint new_val = floatBitsToUint(uintBitsToFloat(expected) + value);\n"
    "        cur = atomicCompSwap(forces[idx], expected, new_val);\n"
    "    } while (cur != expected);\n"
    "}\n";

// Grid cell index a partir de (x, y). El espacio se mapea a [grid_min,
// grid_max] linealmente; valores fuera se clampean al borde para que un
// nodo lejano sea tratado como "esta en la frontera" (no es un disaster —
// solo perdemos algo de precision en la repulsion en esos casos raros).
static const char* k_glsl_cell_idx =
    "uint cell_idx(vec2 p, vec2 grid_min, float cell_size_inv, uint G) {\n"
    "    int cx = int(floor((p.x - grid_min.x) * cell_size_inv));\n"
    "    int cy = int(floor((p.y - grid_min.y) * cell_size_inv));\n"
    "    cx = clamp(cx, 0, int(G) - 1);\n"
    "    cy = clamp(cy, 0, int(G) - 1);\n"
    "    return uint(cy) * G + uint(cx);\n"
    "}\n";

static const char* k_shader_clear =
    "#version 430 core\n"
    "layout(local_size_x = 64) in;\n"
    "layout(std430, binding = 2) buffer Forces      { uint  forces[]; };\n"
    "layout(std430, binding = 6) buffer GridCounts  { uint  grid_counts[]; };\n"
    "layout(std430, binding = 8) buffer Energy      { uint  energy[]; };\n"
    "uniform uint u_num_nodes;\n"
    "uniform uint u_grid_cells;\n"
    "void main() {\n"
    "    uint i = gl_GlobalInvocationID.x;\n"
    "    if (i < u_num_nodes * 2u) forces[i] = 0u;\n"
    "    if (i < u_grid_cells)     grid_counts[i] = 0u;\n"
    "    if (i == 0u)              energy[0] = 0u;\n"
    "}\n";

static const char* k_shader_build_grid =
    "#version 430 core\n"
    "layout(local_size_x = 64) in;\n"
    "layout(std430, binding = 0) buffer Positions   { vec2  positions[]; };\n"
    "layout(std430, binding = 6) buffer GridCounts  { uint  grid_counts[]; };\n"
    "layout(std430, binding = 7) buffer GridCells   { uint  grid_cells[]; };\n"
    "uniform uint  u_num_nodes;\n"
    "uniform uint  u_grid_side;\n"
    "uniform uint  u_grid_K;\n"
    "uniform vec2  u_grid_min;\n"
    "uniform float u_cell_size_inv;\n"
    "void main() {\n"
    "    uint i = gl_GlobalInvocationID.x;\n"
    "    if (i >= u_num_nodes) return;\n"
    "    vec2 p = positions[i];\n"
    "    int cx = int(floor((p.x - u_grid_min.x) * u_cell_size_inv));\n"
    "    int cy = int(floor((p.y - u_grid_min.y) * u_cell_size_inv));\n"
    "    cx = clamp(cx, 0, int(u_grid_side) - 1);\n"
    "    cy = clamp(cy, 0, int(u_grid_side) - 1);\n"
    "    uint ci = uint(cy) * u_grid_side + uint(cx);\n"
    "    uint slot = atomicAdd(grid_counts[ci], 1u);\n"
    "    if (slot < u_grid_K) grid_cells[ci * u_grid_K + slot] = i;\n"
    "}\n";

// Repulsion: 1 thread por nodo. Recorre las 9 celdas vecinas (3x3 alrededor
// de la propia). Cada thread escribe SOLO a su slot forces[2*i],
// forces[2*i+1] -> no necesitamos atomic en esta pasada. Usamos atomicAdd
// igualmente para mantener consistencia con attraction (cero contention,
// coste despreciable).
static const char* k_shader_repulsion =
    "#version 430 core\n"
    "layout(local_size_x = 64) in;\n"
    "layout(std430, binding = 0) buffer Positions   { vec2  positions[]; };\n"
    "layout(std430, binding = 2) buffer Forces      { uint  forces[]; };\n"
    "layout(std430, binding = 3) buffer Flags       { uint  flags[]; };\n"
    "layout(std430, binding = 6) buffer GridCounts  { uint  grid_counts[]; };\n"
    "layout(std430, binding = 7) buffer GridCells   { uint  grid_cells[]; };\n"
    "uniform uint  u_num_nodes;\n"
    "uniform uint  u_grid_side;\n"
    "uniform uint  u_grid_K;\n"
    "uniform vec2  u_grid_min;\n"
    "uniform float u_cell_size_inv;\n"
    "uniform float u_repulsion;\n"
    "uniform float u_min_distance;\n"
    "uniform uint  u_pinned_mask;\n"
    "void main() {\n"
    "    uint i = gl_GlobalInvocationID.x;\n"
    "    if (i >= u_num_nodes) return;\n"
    "    if ((flags[i] & u_pinned_mask) != 0u) return;\n"
    "    vec2 pi = positions[i];\n"
    "    int cx = int(floor((pi.x - u_grid_min.x) * u_cell_size_inv));\n"
    "    int cy = int(floor((pi.y - u_grid_min.y) * u_cell_size_inv));\n"
    "    cx = clamp(cx, 0, int(u_grid_side) - 1);\n"
    "    cy = clamp(cy, 0, int(u_grid_side) - 1);\n"
    "    vec2 fsum = vec2(0.0);\n"
    "    for (int dy = -1; dy <= 1; ++dy) {\n"
    "        int ny = cy + dy;\n"
    "        if (ny < 0 || ny >= int(u_grid_side)) continue;\n"
    "        for (int dx = -1; dx <= 1; ++dx) {\n"
    "            int nx = cx + dx;\n"
    "            if (nx < 0 || nx >= int(u_grid_side)) continue;\n"
    "            uint ci = uint(ny) * u_grid_side + uint(nx);\n"
    "            uint cnt = min(grid_counts[ci], u_grid_K);\n"
    "            for (uint k = 0u; k < cnt; ++k) {\n"
    "                uint j = grid_cells[ci * u_grid_K + k];\n"
    "                if (j == i) continue;\n"
    "                vec2 d = pi - positions[j];\n"
    "                float dist2 = d.x * d.x + d.y * d.y;\n"
    "                float dist = sqrt(dist2);\n"
    "                if (dist < u_min_distance) dist = u_min_distance;\n"
    "                float force = u_repulsion / (dist * dist);\n"
    "                fsum += force * d / dist;\n"
    "            }\n"
    "        }\n"
    "    }\n"
    "    // sin contention: solo este thread escribe a forces[2*i..2*i+1]\n"
    "    forces[2u * i + 0u] = floatBitsToUint(fsum.x);\n"
    "    forces[2u * i + 1u] = floatBitsToUint(fsum.y);\n"
    "}\n";

static const char* k_shader_attraction =
    "#version 430 core\n"
    "layout(local_size_x = 64) in;\n"
    "layout(std430, binding = 0) buffer Positions   { vec2  positions[]; };\n"
    "layout(std430, binding = 2) buffer Forces      { uint  forces[]; };\n"
    "layout(std430, binding = 3) buffer Flags       { uint  flags[]; };\n"
    "layout(std430, binding = 4) buffer Edges       { uvec2 edges[]; };\n"
    "layout(std430, binding = 5) buffer Weights     { float weights[]; };\n"
    "uniform uint  u_num_edges;\n"
    "uniform uint  u_num_nodes;\n"
    "uniform float u_attraction;\n"
    "uniform float u_min_distance;\n"
    "uniform uint  u_pinned_mask;\n"
    // atomic float add via CAS — duplicado inline para acceder al SSBO
    "void atomic_add_float(uint idx, float value) {\n"
    "    uint cur = forces[idx];\n"
    "    uint expected;\n"
    "    do {\n"
    "        expected = cur;\n"
    "        uint new_val = floatBitsToUint(uintBitsToFloat(expected) + value);\n"
    "        cur = atomicCompSwap(forces[idx], expected, new_val);\n"
    "    } while (cur != expected);\n"
    "}\n"
    "void main() {\n"
    "    uint e = gl_GlobalInvocationID.x;\n"
    "    if (e >= u_num_edges) return;\n"
    "    uvec2 ed = edges[e];\n"
    "    uint s = ed.x;\n"
    "    uint t = ed.y;\n"
    "    if (s >= u_num_nodes || t >= u_num_nodes) return;\n"
    "    vec2 d = positions[t] - positions[s];\n"
    "    float dist = length(d);\n"
    "    if (dist < u_min_distance) dist = u_min_distance;\n"
    "    float force = u_attraction * dist * weights[e];\n"
    "    vec2 fxy = force * d / dist;\n"
    "    if ((flags[s] & u_pinned_mask) == 0u) {\n"
    "        atomic_add_float(2u * s + 0u,  fxy.x);\n"
    "        atomic_add_float(2u * s + 1u,  fxy.y);\n"
    "    }\n"
    "    if ((flags[t] & u_pinned_mask) == 0u) {\n"
    "        atomic_add_float(2u * t + 0u, -fxy.x);\n"
    "        atomic_add_float(2u * t + 1u, -fxy.y);\n"
    "    }\n"
    "}\n";

static const char* k_shader_integrate =
    "#version 430 core\n"
    "layout(local_size_x = 64) in;\n"
    "layout(std430, binding = 0) buffer Positions   { vec2  positions[]; };\n"
    "layout(std430, binding = 1) buffer Velocities  { vec2  velocities[]; };\n"
    "layout(std430, binding = 2) buffer Forces      { uint  forces[]; };\n"
    "layout(std430, binding = 3) buffer Flags       { uint  flags[]; };\n"
    "layout(std430, binding = 8) buffer Energy      { uint  energy[]; };\n"
    "uniform uint  u_num_nodes;\n"
    "uniform float u_damping;\n"
    "uniform float u_max_velocity;\n"
    "uniform float u_gravity;\n"
    "uniform uint  u_pinned_mask;\n"
    "void atomic_add_energy(float value) {\n"
    "    uint cur = energy[0];\n"
    "    uint expected;\n"
    "    do {\n"
    "        expected = cur;\n"
    "        uint new_val = floatBitsToUint(uintBitsToFloat(expected) + value);\n"
    "        cur = atomicCompSwap(energy[0], expected, new_val);\n"
    "    } while (cur != expected);\n"
    "}\n"
    "void main() {\n"
    "    uint i = gl_GlobalInvocationID.x;\n"
    "    if (i >= u_num_nodes) return;\n"
    "    if ((flags[i] & u_pinned_mask) != 0u) return;\n"
    "    vec2 p = positions[i];\n"
    "    vec2 v = velocities[i];\n"
    "    vec2 f = vec2(uintBitsToFloat(forces[2u * i + 0u]),\n"
    "                  uintBitsToFloat(forces[2u * i + 1u]));\n"
    "    f -= u_gravity * p; // pull hacia origen\n"
    "    v = v * u_damping + f;\n"
    "    v = clamp(v, vec2(-u_max_velocity), vec2(u_max_velocity));\n"
    "    p += v;\n"
    "    positions[i]  = p;\n"
    "    velocities[i] = v;\n"
    "    atomic_add_energy(v.x * v.x + v.y * v.y);\n"
    "}\n";

// ---------------------------------------------------------------------------
// Shader compile helpers
// ---------------------------------------------------------------------------

static GLuint compile_compute_shader(const char* src) {
    GLuint sh = glCreateShader(GL_COMPUTE_SHADER);
    glShaderSource(sh, 1, &src, nullptr);
    glCompileShader(sh);
    GLint ok = 0;
    glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[2048] = {0};
        glGetShaderInfoLog(sh, sizeof(log), nullptr, log);
        std::fprintf(stderr, "[graph_force_layout_gpu] compute shader compile error:\n%s\n", log);
        glDeleteShader(sh);
        return 0;
    }
    GLuint prog = glCreateProgram();
    glAttachShader(prog, sh);
    glLinkProgram(prog);
    glGetProgramiv(prog, GL_LINK_STATUS, &ok);
    if (!ok) {
        char log[2048] = {0};
        glGetProgramInfoLog(prog, sizeof(log), nullptr, log);
        std::fprintf(stderr, "[graph_force_layout_gpu] compute program link error:\n%s\n", log);
        glDeleteProgram(prog);
        glDeleteShader(sh);
        return 0;
    }
    glDeleteShader(sh);
    return prog;
}

// ---------------------------------------------------------------------------
// State opaco
// ---------------------------------------------------------------------------

struct ForceLayoutGPU {
    int max_nodes = 0;
    int max_edges = 0;
    int grid_side = 64;
    int grid_K    = K_MAX_NODES_PER_CELL;

    int node_count = 0;
    int edge_count = 0;

    // Programs
    GLuint p_clear = 0;
    GLuint p_build = 0;
    GLuint p_repul = 0;
    GLuint p_attr  = 0;
    GLuint p_intg  = 0;

    // SSBOs
    GLuint ssbo_pos    = 0;
    GLuint ssbo_vel    = 0;
    GLuint ssbo_forces = 0;
    GLuint ssbo_flags  = 0;
    GLuint ssbo_edges  = 0;
    GLuint ssbo_weight = 0;
    GLuint ssbo_gcount = 0;
    GLuint ssbo_gcells = 0;
    GLuint ssbo_energy = 0;
};

// ---------------------------------------------------------------------------
// SSBO alloc helper
// ---------------------------------------------------------------------------

static GLuint alloc_ssbo(GLsizeiptr bytes) {
    GLuint b = 0;
    glGenBuffers(1, &b);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, b);
    glBufferData(GL_SHADER_STORAGE_BUFFER, bytes, nullptr, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    return b;
}

// ---------------------------------------------------------------------------
// API
// ---------------------------------------------------------------------------

ForceLayoutGPU* graph_force_layout_gpu_create(int max_nodes, int max_edges,
                                              int grid_cells_per_side) {
    if (max_nodes <= 0 || max_edges < 0) return nullptr;
    if (grid_cells_per_side <= 0) grid_cells_per_side = 64;

    auto* ctx = new ForceLayoutGPU();
    ctx->max_nodes = max_nodes;
    ctx->max_edges = max_edges;
    ctx->grid_side = grid_cells_per_side;

    // Compile shaders. Si alguno falla, abortar limpiamente.
    ctx->p_clear = compile_compute_shader(k_shader_clear);
    ctx->p_build = compile_compute_shader(k_shader_build_grid);
    ctx->p_repul = compile_compute_shader(k_shader_repulsion);
    ctx->p_attr  = compile_compute_shader(k_shader_attraction);
    ctx->p_intg  = compile_compute_shader(k_shader_integrate);
    if (!ctx->p_clear || !ctx->p_build || !ctx->p_repul ||
        !ctx->p_attr  || !ctx->p_intg) {
        graph_force_layout_gpu_destroy(ctx);
        g_gpu_available = false;
        return nullptr;
    }

    // Allocate SSBOs (tamano fijo, dimensionado al max).
    const GLsizeiptr N    = (GLsizeiptr)max_nodes;
    const GLsizeiptr E    = (GLsizeiptr)max_edges;
    const GLsizeiptr G2   = (GLsizeiptr)grid_cells_per_side * grid_cells_per_side;
    const GLsizeiptr K    = (GLsizeiptr)ctx->grid_K;

    ctx->ssbo_pos    = alloc_ssbo(N * 8);          // vec2
    ctx->ssbo_vel    = alloc_ssbo(N * 8);
    ctx->ssbo_forces = alloc_ssbo(N * 8);          // 2 uints/nodo
    ctx->ssbo_flags  = alloc_ssbo(N * 4);          // uint
    ctx->ssbo_edges  = alloc_ssbo(std::max<GLsizeiptr>(E * 8, 8));   // uvec2
    ctx->ssbo_weight = alloc_ssbo(std::max<GLsizeiptr>(E * 4, 4));
    ctx->ssbo_gcount = alloc_ssbo(G2 * 4);
    ctx->ssbo_gcells = alloc_ssbo(G2 * K * 4);
    ctx->ssbo_energy = alloc_ssbo(4);

    g_gpu_available = true;
    return ctx;
}

void graph_force_layout_gpu_destroy(ForceLayoutGPU* ctx) {
    if (!ctx) return;
    if (ctx->p_clear) glDeleteProgram(ctx->p_clear);
    if (ctx->p_build) glDeleteProgram(ctx->p_build);
    if (ctx->p_repul) glDeleteProgram(ctx->p_repul);
    if (ctx->p_attr ) glDeleteProgram(ctx->p_attr );
    if (ctx->p_intg ) glDeleteProgram(ctx->p_intg );
    GLuint bufs[] = {
        ctx->ssbo_pos, ctx->ssbo_vel, ctx->ssbo_forces, ctx->ssbo_flags,
        ctx->ssbo_edges, ctx->ssbo_weight, ctx->ssbo_gcount,
        ctx->ssbo_gcells, ctx->ssbo_energy,
    };
    glDeleteBuffers((GLsizei)(sizeof(bufs)/sizeof(bufs[0])), bufs);
    delete ctx;
}

unsigned int graph_force_layout_gpu_positions_ssbo(const ForceLayoutGPU* ctx) {
    return ctx ? (unsigned int)ctx->ssbo_pos : 0u;
}

void graph_force_layout_gpu_upload(ForceLayoutGPU* ctx, const GraphData& graph) {
    if (!ctx) return;
    int N = std::min(graph.node_count, ctx->max_nodes);
    int E = std::min(graph.edge_count, ctx->max_edges);
    ctx->node_count = N;
    ctx->edge_count = E;
    if (N <= 0) return;

    // Empaquetar SoA temporales (positions vec2, velocities vec2, flags
    // uint, edges uvec2, weights float). Lo hacemos siempre en buffers
    // contiguos para subir con glBufferSubData de una sola pasada por SSBO.
    std::vector<float>    pos(2 * N), vel(2 * N), w((size_t)std::max(E,1));
    std::vector<uint32_t> fl((size_t)N);
    std::vector<uint32_t> ed(2 * (size_t)std::max(E, 1));

    for (int i = 0; i < N; ++i) {
        const GraphNode& n = graph.nodes[i];
        pos[2*i + 0] = n.x;
        pos[2*i + 1] = n.y;
        vel[2*i + 0] = n.vx;
        vel[2*i + 1] = n.vy;
        fl[i]        = (uint32_t)n.flags;
    }
    for (int e = 0; e < E; ++e) {
        const GraphEdge& g = graph.edges[e];
        ed[2*e + 0] = g.source;
        ed[2*e + 1] = g.target;
        w[e]        = g.weight;
    }

    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_pos);
    glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(N * 8), pos.data());
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_vel);
    glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(N * 8), vel.data());
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_flags);
    glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(N * 4), fl.data());
    if (E > 0) {
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_edges);
        glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(E * 8), ed.data());
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_weight);
        glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(E * 4), w.data());
    }
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}

// Bind helper (todos los compute shaders comparten layout).
static void bind_all_ssbos(const ForceLayoutGPU* ctx) {
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ctx->ssbo_pos);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ctx->ssbo_vel);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ctx->ssbo_forces);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, ctx->ssbo_flags);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, ctx->ssbo_edges);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, ctx->ssbo_weight);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, ctx->ssbo_gcount);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 7, ctx->ssbo_gcells);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 8, ctx->ssbo_energy);
}

// Calcula bbox usando un readback rapido de la SSBO de positions. NO actualiza
// el GraphData CPU — solo computa los limites para el grid.
static void compute_grid_bbox(ForceLayoutGPU* ctx,
                              float& x0, float& y0, float& x1, float& y1) {
    int N = ctx->node_count;
    if (N <= 0) { x0 = y0 = -100.0f; x1 = y1 = 100.0f; return; }
    std::vector<float> pos((size_t)2 * N);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_pos);
    glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(N * 8), pos.data());
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    x0 = x1 = pos[0];
    y0 = y1 = pos[1];
    for (int i = 1; i < N; ++i) {
        float px = pos[2*i + 0], py = pos[2*i + 1];
        if (px < x0) x0 = px; if (px > x1) x1 = px;
        if (py < y0) y0 = py; if (py > y1) y1 = py;
    }
    float margin = (x1 - x0 + y1 - y0) * 0.05f + 1.0f;
    x0 -= margin; y0 -= margin; x1 += margin; y1 += margin;
    // Asegurar que el grid es cuadrado y no degenerado.
    float side = std::max(x1 - x0, y1 - y0);
    if (side <= 0.0f) side = 1.0f;
    float cx = (x0 + x1) * 0.5f, cy = (y0 + y1) * 0.5f;
    x0 = cx - side * 0.5f; x1 = cx + side * 0.5f;
    y0 = cy - side * 0.5f; y1 = cy + side * 0.5f;
}

float graph_force_layout_gpu_step(ForceLayoutGPU* ctx, const ForceLayoutConfig& config) {
    if (!ctx || ctx->node_count <= 0) return 0.0f;

    const uint32_t pinned_mask = (uint32_t)NF_PINNED;
    const int N  = ctx->node_count;
    const int E  = ctx->edge_count;
    const int G  = ctx->grid_side;
    const int G2 = G * G;
    const int K  = ctx->grid_K;

    auto group_count = [](int items, int local_size) {
        if (items <= 0) return 1;
        return (items + local_size - 1) / local_size;
    };
    const int gc_node = group_count(N,  64);
    const int gc_edge = group_count(std::max(E, 1), 64);
    const int gc_max  = group_count(std::max({N * 2, G2}), 64);

    float total_energy = 0.0f;

    for (int it = 0; it < std::max(1, config.iterations); ++it) {
        // ---- BBox + grid params ----
        float x0, y0, x1, y1;
        compute_grid_bbox(ctx, x0, y0, x1, y1);
        float side = x1 - x0;
        float cell_size_inv = (float)G / side;

        bind_all_ssbos(ctx);

        // ---- 1. Clear ----
        glUseProgram(ctx->p_clear);
        glUniform1ui(glGetUniformLocation(ctx->p_clear, "u_num_nodes"),  (GLuint)N);
        glUniform1ui(glGetUniformLocation(ctx->p_clear, "u_grid_cells"), (GLuint)G2);
        glDispatchCompute(gc_max, 1, 1);
        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);

        // ---- 2. Build grid ----
        glUseProgram(ctx->p_build);
        glUniform1ui(glGetUniformLocation(ctx->p_build, "u_num_nodes"),     (GLuint)N);
        glUniform1ui(glGetUniformLocation(ctx->p_build, "u_grid_side"),     (GLuint)G);
        glUniform1ui(glGetUniformLocation(ctx->p_build, "u_grid_K"),        (GLuint)K);
        glUniform2f (glGetUniformLocation(ctx->p_build, "u_grid_min"),      x0, y0);
        glUniform1f (glGetUniformLocation(ctx->p_build, "u_cell_size_inv"), cell_size_inv);
        glDispatchCompute(gc_node, 1, 1);
        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);

        // ---- 3. Repulsion ----
        glUseProgram(ctx->p_repul);
        glUniform1ui(glGetUniformLocation(ctx->p_repul, "u_num_nodes"),     (GLuint)N);
        glUniform1ui(glGetUniformLocation(ctx->p_repul, "u_grid_side"),     (GLuint)G);
        glUniform1ui(glGetUniformLocation(ctx->p_repul, "u_grid_K"),        (GLuint)K);
        glUniform2f (glGetUniformLocation(ctx->p_repul, "u_grid_min"),      x0, y0);
        glUniform1f (glGetUniformLocation(ctx->p_repul, "u_cell_size_inv"), cell_size_inv);
        glUniform1f (glGetUniformLocation(ctx->p_repul, "u_repulsion"),     config.repulsion);
        glUniform1f (glGetUniformLocation(ctx->p_repul, "u_min_distance"),  config.min_distance);
        glUniform1ui(glGetUniformLocation(ctx->p_repul, "u_pinned_mask"),   pinned_mask);
        glDispatchCompute(gc_node, 1, 1);
        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);

        // ---- 4. Attraction ----
        if (E > 0) {
            glUseProgram(ctx->p_attr);
            glUniform1ui(glGetUniformLocation(ctx->p_attr, "u_num_edges"),    (GLuint)E);
            glUniform1ui(glGetUniformLocation(ctx->p_attr, "u_num_nodes"),    (GLuint)N);
            glUniform1f (glGetUniformLocation(ctx->p_attr, "u_attraction"),   config.attraction);
            glUniform1f (glGetUniformLocation(ctx->p_attr, "u_min_distance"), config.min_distance);
            glUniform1ui(glGetUniformLocation(ctx->p_attr, "u_pinned_mask"),  pinned_mask);
            glDispatchCompute(gc_edge, 1, 1);
            glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
        }

        // ---- 5. Integrate ----
        glUseProgram(ctx->p_intg);
        glUniform1ui(glGetUniformLocation(ctx->p_intg, "u_num_nodes"),    (GLuint)N);
        glUniform1f (glGetUniformLocation(ctx->p_intg, "u_damping"),      config.damping);
        glUniform1f (glGetUniformLocation(ctx->p_intg, "u_max_velocity"), config.max_velocity);
        glUniform1f (glGetUniformLocation(ctx->p_intg, "u_gravity"),      config.gravity);
        glUniform1ui(glGetUniformLocation(ctx->p_intg, "u_pinned_mask"),  pinned_mask);
        glDispatchCompute(gc_node, 1, 1);
        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);

        // ---- Lectura de energia (uint→float, atomic-CAS desde GPU) ----
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_energy);
        uint32_t energy_bits = 0;
        glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, 4, &energy_bits);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
        std::memcpy(&total_energy, &energy_bits, 4);
    }

    glUseProgram(0);
    return total_energy;
}

void graph_force_layout_gpu_readback(ForceLayoutGPU* ctx, GraphData& graph,
                                     bool include_velocities) {
    if (!ctx || ctx->node_count <= 0) return;
    int N = std::min(ctx->node_count, graph.node_count);

    std::vector<float> pos((size_t)2 * N);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_pos);
    glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(N * 8), pos.data());

    std::vector<float> vel;
    if (include_velocities) {
        vel.resize((size_t)2 * N);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ctx->ssbo_vel);
        glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(N * 8), vel.data());
    }
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);

    for (int i = 0; i < N; ++i) {
        graph.nodes[i].x = pos[2*i + 0];
        graph.nodes[i].y = pos[2*i + 1];
        if (include_velocities) {
            graph.nodes[i].vx = vel[2*i + 0];
            graph.nodes[i].vy = vel[2*i + 1];
        }
    }
    graph.update_bounds();
}
