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

#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>

namespace graph {

// ---------------------------------------------------------------------------
// layout_grid
// ---------------------------------------------------------------------------
void layout_grid(GraphData& g, float spacing) {
    if (g.node_count <= 0) return;
    int cols = (int)std::ceil(std::sqrt((float)g.node_count));
    if (cols < 1) cols = 1;
    int rows = (g.node_count + cols - 1) / cols;
    float ox = -0.5f * (cols - 1) * spacing;
    float oy = -0.5f * (rows - 1) * spacing;
    for (int i = 0; i < g.node_count; ++i) {
        GraphNode& n = g.nodes[i];
        if (n.flags & NF_PINNED) continue;
        int col = i % cols;
        int row = i / cols;
        n.x  = ox + col * spacing;
        n.y  = oy + row * spacing;
        n.vx = 0.0f;
        n.vy = 0.0f;
    }
    g.update_bounds();
}

// ---------------------------------------------------------------------------
// layout_circular
// ---------------------------------------------------------------------------
void layout_circular(GraphData& g, float radius) {
    if (g.node_count <= 0) return;
    const float two_pi = 6.28318530718f;
    for (int i = 0; i < g.node_count; ++i) {
        GraphNode& n = g.nodes[i];
        if (n.flags & NF_PINNED) continue;
        float angle = two_pi * (float)i / (float)g.node_count;
        n.x  = radius * std::cos(angle);
        n.y  = radius * std::sin(angle);
        n.vx = 0.0f;
        n.vy = 0.0f;
    }
    g.update_bounds();
}

// ---------------------------------------------------------------------------
// layout_random
// ---------------------------------------------------------------------------
void layout_random(GraphData& g, float spread) {
    if (g.node_count <= 0) return;
    for (int i = 0; i < g.node_count; ++i) {
        GraphNode& n = g.nodes[i];
        if (n.flags & NF_PINNED) continue;
        n.x  = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
        n.y  = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
        n.vx = 0.0f;
        n.vy = 0.0f;
    }
    g.update_bounds();
}

// ---------------------------------------------------------------------------
// layout_radial — BFS desde root, anillos concentricos por hop
// ---------------------------------------------------------------------------
void layout_radial(GraphData& g, int root_node, float ring_spacing) {
    if (g.node_count <= 0) return;
    if (root_node < 0 || root_node >= g.node_count) root_node = 0;

    // Adyacencia no dirigida via CSR temporal — para grafos pequenios/medios
    // basta con vector<vector<int>>.
    std::vector<std::vector<int>> adj(g.node_count);
    for (int e = 0; e < g.edge_count; ++e) {
        int s = (int)g.edges[e].source;
        int t = (int)g.edges[e].target;
        if (s < 0 || s >= g.node_count) continue;
        if (t < 0 || t >= g.node_count) continue;
        adj[s].push_back(t);
        adj[t].push_back(s);
    }

    // BFS para asignar hop (-1 = no visitado todavia)
    std::vector<int> hop(g.node_count, -1);
    hop[root_node] = 0;
    std::queue<int> q;
    q.push(root_node);
    int max_hop = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (hop[v] == -1) {
                hop[v] = hop[u] + 1;
                if (hop[v] > max_hop) max_hop = hop[v];
                q.push(v);
            }
        }
    }

    // Nodos no alcanzables van al hop max_hop + 1
    int orphan_hop = max_hop + 1;
    bool has_orphans = false;
    for (int i = 0; i < g.node_count; ++i) {
        if (hop[i] == -1) { hop[i] = orphan_hop; has_orphans = true; }
    }

    // Agrupar indices por hop
    int total_hops = (has_orphans ? orphan_hop : max_hop) + 1;
    std::vector<std::vector<int>> by_hop(total_hops);
    for (int i = 0; i < g.node_count; ++i) by_hop[hop[i]].push_back(i);

    const float two_pi = 6.28318530718f;
    for (int k = 0; k < total_hops; ++k) {
        const auto& ring = by_hop[k];
        if (ring.empty()) continue;
        if (k == 0) {
            // root en el centro
            GraphNode& n = g.nodes[ring[0]];
            if (!(n.flags & NF_PINNED)) {
                n.x = 0.0f; n.y = 0.0f; n.vx = 0.0f; n.vy = 0.0f;
            }
            continue;
        }
        float radius = (float)k * ring_spacing;
        int count = (int)ring.size();
        for (int j = 0; j < count; ++j) {
            GraphNode& n = g.nodes[ring[j]];
            if (n.flags & NF_PINNED) continue;
            float angle = two_pi * (float)j / (float)count;
            n.x  = radius * std::cos(angle);
            n.y  = radius * std::sin(angle);
            n.vx = 0.0f;
            n.vy = 0.0f;
        }
    }
    g.update_bounds();
}

// ---------------------------------------------------------------------------
// layout_hierarchical — niveles por longest-path BFS + baricentro greedy
// ---------------------------------------------------------------------------
void layout_hierarchical(GraphData& g, int direction,
                         float layer_spacing, float node_spacing) {
    if (g.node_count <= 0) return;

    const int N = g.node_count;

    // 1) Calcular in-degree para encontrar las raices
    std::vector<int> in_deg(N, 0);
    std::vector<std::vector<int>> out_adj(N);
    std::vector<std::vector<int>> in_adj(N);
    for (int e = 0; e < g.edge_count; ++e) {
        int s = (int)g.edges[e].source;
        int t = (int)g.edges[e].target;
        if (s < 0 || s >= N || t < 0 || t >= N) continue;
        if (s == t) continue;
        out_adj[s].push_back(t);
        in_adj[t].push_back(s);
        in_deg[t]++;
    }

    // 2) Asignar nivel: longest-path desde nodos sin in-edges. BFS multi-source
    //    con relax: level[v] = max(level[v], level[u]+1).
    std::vector<int> level(N, 0);
    std::vector<int> order; order.reserve(N);
    std::queue<int> q;
    for (int i = 0; i < N; ++i) {
        if (in_deg[i] == 0) { q.push(i); }
    }
    // Si todo el grafo tiene ciclos (no hay raices), forzar nodo 0 como raiz
    if (q.empty()) { q.push(0); level[0] = 0; }

    // BFS con propagacion. Para evitar bucles infinitos en grafos ciclicos,
    // limitamos la profundidad a N.
    std::vector<int> visited(N, 0);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        if (visited[u]++ > N) continue;
        for (int v : out_adj[u]) {
            int new_lv = level[u] + 1;
            if (new_lv > level[v]) {
                level[v] = new_lv;
                if (level[v] < N) q.push(v);
            }
        }
    }

    int max_level = 0;
    for (int i = 0; i < N; ++i) {
        if (level[i] > max_level) max_level = level[i];
    }

    // 3) Agrupar nodos por nivel
    std::vector<std::vector<int>> by_level(max_level + 1);
    for (int i = 0; i < N; ++i) by_level[level[i]].push_back(i);

    // 4) Reducir cruces: por cada nivel L > 0 ordenar por baricentro de los
    //    indices (en el array del nivel L-1) de sus padres. Greedy, no optimo.
    std::vector<int> pos_in_level(N, 0);
    for (int j = 0; j < (int)by_level[0].size(); ++j) pos_in_level[by_level[0][j]] = j;

    for (int L = 1; L <= max_level; ++L) {
        auto& cur = by_level[L];
        std::vector<float> bary(cur.size(), 0.0f);
        for (size_t i = 0; i < cur.size(); ++i) {
            int v = cur[i];
            float sum = 0.0f; int cnt = 0;
            for (int p : in_adj[v]) {
                if (level[p] == L - 1) {
                    sum += (float)pos_in_level[p];
                    cnt++;
                }
            }
            bary[i] = (cnt > 0) ? (sum / (float)cnt) : (float)i;
        }
        // Ordenar `cur` por bary
        std::vector<size_t> idx(cur.size());
        for (size_t i = 0; i < idx.size(); ++i) idx[i] = i;
        std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b){
            if (bary[a] != bary[b]) return bary[a] < bary[b];
            return cur[a] < cur[b];
        });
        std::vector<int> reordered(cur.size());
        for (size_t i = 0; i < idx.size(); ++i) reordered[i] = cur[idx[i]];
        cur = std::move(reordered);
        for (int j = 0; j < (int)cur.size(); ++j) pos_in_level[cur[j]] = j;
    }

    // 5) Posiciones. layer_axis = nivel * layer_spacing; transverse_axis =
    //    (j - (size-1)/2) * node_spacing (centrado).
    auto place = [&](int node_idx, float layer_axis, float transverse_axis) {
        GraphNode& n = g.nodes[node_idx];
        if (n.flags & NF_PINNED) return;
        float lx = 0.0f, ly = 0.0f;
        switch (direction) {
            case 0: // LR
                lx =  layer_axis; ly = transverse_axis; break;
            case 1: // RL
                lx = -layer_axis; ly = transverse_axis; break;
            case 2: // TB
                lx = transverse_axis; ly =  layer_axis; break;
            case 3: // BT
                lx = transverse_axis; ly = -layer_axis; break;
            default:
                lx =  layer_axis; ly = transverse_axis; break;
        }
        n.x  = lx;
        n.y  = ly;
        n.vx = 0.0f;
        n.vy = 0.0f;
    };

    // Centramos cada nivel en torno a 0 en el eje transversal y arrancamos en
    // 0 en el eje de capas.
    for (int L = 0; L <= max_level; ++L) {
        const auto& cur = by_level[L];
        int sz = (int)cur.size();
        if (sz == 0) continue;
        float layer_axis = (float)L * layer_spacing;
        float t0 = -0.5f * (sz - 1) * node_spacing;
        for (int j = 0; j < sz; ++j) {
            place(cur[j], layer_axis, t0 + j * node_spacing);
        }
    }
    g.update_bounds();
}

// ---------------------------------------------------------------------------
// layout_fixed — no-op. Existe para que el caller pueda usar "fixed" en un
// switch sin casos especiales.
// ---------------------------------------------------------------------------
void layout_fixed(GraphData& /*g*/) {
    // intencionalmente vacio
}

} // namespace graph
