#include "viz/graph_viewport.h"
#include "viz/graph_types.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "core/graph_spatial_hash.h"
#include "imgui.h"

#include <algorithm>
#include <cstdio>   // snprintf
#include <cstring>  // memset
#include <vector>

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

static void viewport_to_graph(float vx, float vy,
                               float widget_x, float widget_y,
                               float widget_w, float widget_h,
                               float cam_x, float cam_y, float zoom,
                               float& gx, float& gy)
{
    gx = (vx - widget_x - widget_w * 0.5f) / zoom + cam_x;
    gy = (vy - widget_y - widget_h * 0.5f) / zoom + cam_y;
}

static void graph_to_viewport(float gx, float gy,
                              float widget_x, float widget_y,
                              float widget_w, float widget_h,
                              float cam_x, float cam_y, float zoom,
                              float& vx, float& vy)
{
    vx = (gx - cam_x) * zoom + widget_x + widget_w * 0.5f;
    vy = (gy - cam_y) * zoom + widget_y + widget_h * 0.5f;
}

// ---------------------------------------------------------------------------
// graph_viewport_fit
// ---------------------------------------------------------------------------

void graph_viewport_fit(GraphData& graph, GraphViewportState& state)
{
    graph.update_bounds();
    if (graph.node_count == 0) {
        state.cam_x = 0.0f;
        state.cam_y = 0.0f;
        state.zoom  = 1.0f;
        return;
    }

    float cx = (graph.min_x + graph.max_x) * 0.5f;
    float cy = (graph.min_y + graph.max_y) * 0.5f;
    state.cam_x = cx;
    state.cam_y = cy;

    float span_x = graph.max_x - graph.min_x;
    float span_y = graph.max_y - graph.min_y;
    float span   = (span_x > span_y ? span_x : span_y);

    // Use render dimensions if available; fall back to a safe default.
    float view_px = (state.render_w > 0 ? (float)state.render_w : 600.0f);
    float view_py = (state.render_h > 0 ? (float)state.render_h : 400.0f);
    float view_min = (view_px < view_py ? view_px : view_py);

    if (span > 0.0f) {
        state.zoom = (view_min * 0.9f) / span;
    } else {
        state.zoom = 1.0f;
    }

    // Clamp to allowed range
    if (state.zoom < state.zoom_min) state.zoom = state.zoom_min;
    if (state.zoom > state.zoom_max) state.zoom = state.zoom_max;
}

// ---------------------------------------------------------------------------
// graph_viewport_destroy
// ---------------------------------------------------------------------------

void graph_viewport_destroy(GraphViewportState& state)
{
    if (state.renderer) {
        graph_renderer_destroy(state.renderer);
        state.renderer = nullptr;
    }
    if (state.spatial) {
        delete state.spatial;
        state.spatial = nullptr;
    }
    state.initialized = false;
}

// ---------------------------------------------------------------------------
// graph_viewport — main widget
// ---------------------------------------------------------------------------

bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
                    ImVec2 size, const GraphViewportCallbacks& cb)
{
    bool interacted = false;

    // Resolve size
    ImVec2 avail = ImGui::GetContentRegionAvail();
    float w = (size.x > 0.0f) ? size.x : avail.x;
    float h = (size.y > 0.0f) ? size.y : avail.y;
    if (w < 1.0f) w = 1.0f;
    if (h < 1.0f) h = 1.0f;

    int iw = (int)w, ih = (int)h;

    // -------------------------------------------------------------------
    // 1. Lazy init
    // -------------------------------------------------------------------
    if (!state.initialized) {
        state.renderer  = graph_renderer_create(iw, ih);
        state.spatial   = new SpatialHash(20.0f, 4096);
        state.render_w  = iw;
        state.render_h  = ih;
        state.initialized = true;
        graph_viewport_fit(graph, state);
    }

    // -------------------------------------------------------------------
    // 2. Resize
    // -------------------------------------------------------------------
    if (iw != state.render_w || ih != state.render_h) {
        graph_renderer_resize(state.renderer, iw, ih);
        state.render_w = iw;
        state.render_h = ih;
    }

    // -------------------------------------------------------------------
    // 3. Force layout step
    // -------------------------------------------------------------------
    if (state.layout_running && graph.node_count > 0) {
        state.layout_energy = graph_force_layout_step(graph);
        if (state.layout_energy < 0.01f) {
            state.layout_running = false;
        }
    }

    // -------------------------------------------------------------------
    // 4. Build spatial hash
    // -------------------------------------------------------------------
    if (graph.node_count > 0) {
        static std::vector<float> xs_buf, ys_buf, sz_buf;
        xs_buf.resize(graph.node_count);
        ys_buf.resize(graph.node_count);
        sz_buf.resize(graph.node_count);
        for (int i = 0; i < graph.node_count; ++i) {
            xs_buf[i] = graph.nodes[i].x;
            ys_buf[i] = graph.nodes[i].y;
            sz_buf[i] = resolve_node_size(graph.nodes[i],
                                           graph.types, graph.type_count);
        }
        state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count);
    }

    // -------------------------------------------------------------------
    // 5. Invisible button to capture input
    // -------------------------------------------------------------------
    ImGui::PushID(id);

    ImVec2 widget_pos = ImGui::GetCursorScreenPos();
    ImGui::InvisibleButton("canvas", ImVec2(w, h),
                           ImGuiButtonFlags_MouseButtonLeft  |
                           ImGuiButtonFlags_MouseButtonMiddle|
                           ImGuiButtonFlags_MouseButtonRight);

    bool hovered  = ImGui::IsItemHovered();
    bool lm_down  = ImGui::IsMouseDown(ImGuiMouseButton_Left);
    bool lm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
    bool lm_release= ImGui::IsMouseReleased(ImGuiMouseButton_Left);
    bool lm_dbl   = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
    bool mm_down  = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
    bool rm_down  = ImGui::IsMouseDown(ImGuiMouseButton_Right);
    bool rm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
    bool shift    = ImGui::GetIO().KeyShift;
    bool ctrl     = ImGui::GetIO().KeyCtrl;

    ImVec2 mouse_pos = ImGui::GetMousePos();
    float  mx = mouse_pos.x, my = mouse_pos.y;

    // Convert mouse to graph space
    float gx_mouse, gy_mouse;
    viewport_to_graph(mx, my,
                      widget_pos.x, widget_pos.y, w, h,
                      state.cam_x, state.cam_y, state.zoom,
                      gx_mouse, gy_mouse);

    // -------------------------------------------------------------------
    // 5a. Pan (middle mouse drag, o right-button drag SIN nodo bajo cursor)
    // -------------------------------------------------------------------
    // El pan con boton derecho se inhibe si el right-click cae sobre un
    // nodo: en ese caso queremos mostrar el menu contextual via callback.
    if (hovered && (mm_down || (rm_down && state.hovered_node < 0))) {
        ImVec2 delta = ImGui::GetIO().MouseDelta;
        if (delta.x != 0.0f || delta.y != 0.0f) {
            state.cam_x -= delta.x / state.zoom;
            state.cam_y -= delta.y / state.zoom;
            interacted = true;
        }
    }

    // -------------------------------------------------------------------
    // 5b. Zoom (scroll wheel)
    // -------------------------------------------------------------------
    if (hovered) {
        float wheel = ImGui::GetIO().MouseWheel;
        if (wheel != 0.0f) {
            float old_zoom = state.zoom;
            float new_zoom = old_zoom * (1.0f + wheel * 0.1f);
            if (new_zoom < state.zoom_min) new_zoom = state.zoom_min;
            if (new_zoom > state.zoom_max) new_zoom = state.zoom_max;

            float rel_x = (mx - widget_pos.x - w * 0.5f);
            float rel_y = (my - widget_pos.y - h * 0.5f);
            state.cam_x += rel_x / old_zoom - rel_x / new_zoom;
            state.cam_y += rel_y / old_zoom - rel_y / new_zoom;
            state.zoom   = new_zoom;
            interacted   = true;

            ImGui::GetIO().MouseWheel = 0.0f;
        }
    }

    // -------------------------------------------------------------------
    // 5c. Hover — query nearest node
    // -------------------------------------------------------------------
    int prev_hovered = state.hovered_node;
    if (prev_hovered >= 0 && prev_hovered < graph.node_count) {
        graph.nodes[prev_hovered].flags &= ~NF_HOVERED;
    }
    state.hovered_node = -1;
    if (hovered && graph.node_count > 0) {
        // Spatial query con un radio amplio para descartar lo lejano; despues
        // refinamos comparando con el tamaño real (en world units) del nodo
        // candidato + un pequeño margen, asi el hover solo se activa cuando el
        // raton esta efectivamente sobre el nodo.
        float coarse_radius = 24.0f / state.zoom;
        float dist = 0.0f;
        int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, coarse_radius, &dist);
        if (nearest >= 0 && nearest < graph.node_count) {
            float node_r_px = resolve_node_size(graph.nodes[nearest],
                                                 graph.types, graph.type_count);
            float hit_r_world = (node_r_px + 2.0f) / state.zoom; // 2 px margen
            if (dist <= hit_r_world) {
                state.hovered_node = nearest;
                graph.nodes[nearest].flags |= NF_HOVERED;
                interacted = true;
            }
        }
    }

    // -------------------------------------------------------------------
    // 5d. Lasso (shift + drag sobre area vacia)
    // -------------------------------------------------------------------
    // Activacion: shift presionado + lm_click (no haciendo click sobre nodo).
    if (hovered && lm_click && shift && state.hovered_node < 0
        && state.drag_node == -1 && !state.lasso_active) {
        state.lasso_active = true;
        state.lasso_start  = ImVec2(gx_mouse, gy_mouse);
        state.lasso_end    = ImVec2(gx_mouse, gy_mouse);
    }
    if (state.lasso_active) {
        state.lasso_end = ImVec2(gx_mouse, gy_mouse);
        if (lm_release) {
            // Calcular AABB en world space y anadir nodos contenidos a la seleccion
            float x0 = std::min(state.lasso_start.x, state.lasso_end.x);
            float x1 = std::max(state.lasso_start.x, state.lasso_end.x);
            float y0 = std::min(state.lasso_start.y, state.lasso_end.y);
            float y1 = std::max(state.lasso_start.y, state.lasso_end.y);
            for (int i = 0; i < graph.node_count; ++i) {
                const GraphNode& n = graph.nodes[i];
                if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1) {
                    graph_viewport_add_to_selection(graph, state, i);
                }
            }
            state.lasso_active = false;
            interacted = true;
        }
    }

    // -------------------------------------------------------------------
    // 5e. Node drag (left mouse down on a node)
    // -------------------------------------------------------------------
    // Si el nodo bajo el cursor esta seleccionado, arrastra TODA la seleccion;
    // en otro caso solo el nodo bajo el cursor.
    if (hovered && lm_down && !state.lasso_active) {
        if (state.drag_node == -1 && state.hovered_node >= 0) {
            state.drag_node      = state.hovered_node;
            state.is_dragging    = true;
            state.drag_anchor_x  = gx_mouse;
            state.drag_anchor_y  = gy_mouse;
            // Si el nodo no esta en la seleccion y no es un toggle, lo
            // anadimos para que el drag mueva al menos ese nodo.
            if (!graph_viewport_is_selected(state, state.drag_node)) {
                if (!ctrl) {
                    // Reemplaza seleccion: drag de nodo no seleccionado
                    graph_viewport_clear_selection(graph, state);
                }
                graph_viewport_add_to_selection(graph, state, state.drag_node);
            }
            // Fijar todos los nodos seleccionados como pinned mientras dura
            // el drag — el force layout no los movera.
            for (int idx : state.selection) {
                if (idx >= 0 && idx < graph.node_count) {
                    graph.nodes[idx].flags |= NF_PINNED;
                }
            }
        }
    } else if (state.drag_node >= 0) {
        // Release: mantener pinned (deja al usuario re-arrastrar). Quien
        // quiera "soltar" el pin puede hacer Esc o un re-click sobre area
        // vacia para limpiar seleccion.
        state.drag_node   = -1;
        state.is_dragging = false;
    }

    if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
        // delta en world space respecto al frame previo
        float dx = ImGui::GetIO().MouseDelta.x / state.zoom;
        float dy = ImGui::GetIO().MouseDelta.y / state.zoom;
        if (dx != 0.0f || dy != 0.0f) {
            for (int idx : state.selection) {
                if (idx >= 0 && idx < graph.node_count) {
                    GraphNode& n = graph.nodes[idx];
                    n.x  += dx;
                    n.y  += dy;
                    n.vx  = 0.0f;
                    n.vy  = 0.0f;
                    n.flags |= NF_PINNED;
                }
            }
            interacted = true;
        }
    }

    // -------------------------------------------------------------------
    // 5f. Click — seleccion (single / ctrl-toggle)
    // -------------------------------------------------------------------
    // Solo procesar clicks que NO son inicio de drag (es decir: el lm_click
    // ya pudo haber lanzado un drag arriba). La rama drag arriba ya gestiona
    // la seleccion en su caso. Aqui nos ocupa el caso de click en area vacia
    // o click puro sobre nodo sin shift/lasso.
    if (hovered && lm_click && !shift && state.drag_node == -1
        && !state.lasso_active) {
        if (state.hovered_node >= 0) {
            if (ctrl) {
                graph_viewport_toggle_selection(graph, state, state.hovered_node);
            } else {
                graph_viewport_clear_selection(graph, state);
                graph_viewport_add_to_selection(graph, state, state.hovered_node);
            }
        } else {
            graph_viewport_clear_selection(graph, state);
        }
        interacted = true;
    }

    // -------------------------------------------------------------------
    // 5g. Right-click sobre nodo -> on_context_menu
    // -------------------------------------------------------------------
    if (hovered && rm_click && state.hovered_node >= 0 && cb.on_context_menu) {
        cb.on_context_menu(state.hovered_node, mouse_pos, cb.user);
        interacted = true;
    }

    // -------------------------------------------------------------------
    // 5h. Double-click -> on_double_click
    // -------------------------------------------------------------------
    if (hovered && lm_dbl && state.hovered_node >= 0 && cb.on_double_click) {
        cb.on_double_click(state.hovered_node, cb.user);
        interacted = true;
    }

    // -------------------------------------------------------------------
    // 5i. Keyboard shortcuts (only when widget hovered)
    // -------------------------------------------------------------------
    if (hovered) {
        if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
            state.layout_running = !state.layout_running;
        }
        if (ImGui::IsKeyPressed(ImGuiKey_F)) {
            graph_viewport_fit(graph, state);
            interacted = true;
        }
        if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            graph_viewport_clear_selection(graph, state);
            interacted = true;
        }
    }

    // -------------------------------------------------------------------
    // 6. Render to GPU texture
    // -------------------------------------------------------------------
    unsigned int tex_id = graph_renderer_draw(state.renderer, graph,
                                              state.cam_x, state.cam_y,
                                              state.zoom);

    // -------------------------------------------------------------------
    // 7. Display texture (flip UV for OpenGL FBO convention)
    // -------------------------------------------------------------------
    ImDrawList* draw_list = ImGui::GetWindowDrawList();
    draw_list->AddImage(
        (ImTextureID)(intptr_t)tex_id,
        widget_pos,
        ImVec2(widget_pos.x + w, widget_pos.y + h),
        ImVec2(0.0f, 1.0f),
        ImVec2(1.0f, 0.0f)
    );

    // -------------------------------------------------------------------
    // 7b. Lasso visual feedback (rect en pantalla)
    // -------------------------------------------------------------------
    if (state.lasso_active) {
        float vx0, vy0, vx1, vy1;
        graph_to_viewport(state.lasso_start.x, state.lasso_start.y,
                          widget_pos.x, widget_pos.y, w, h,
                          state.cam_x, state.cam_y, state.zoom, vx0, vy0);
        graph_to_viewport(state.lasso_end.x, state.lasso_end.y,
                          widget_pos.x, widget_pos.y, w, h,
                          state.cam_x, state.cam_y, state.zoom, vx1, vy1);
        ImVec2 a(std::min(vx0, vx1), std::min(vy0, vy1));
        ImVec2 b(std::max(vx0, vx1), std::max(vy0, vy1));
        draw_list->AddRectFilled(a, b, IM_COL32(80, 140, 220, 40));
        draw_list->AddRect      (a, b, IM_COL32(120, 180, 240, 200));
    }

    // -------------------------------------------------------------------
    // 8. Tooltip on hovered node (suprimido durante drag o lasso para no
    // tapar la accion)
    // -------------------------------------------------------------------
    if (state.hovered_node >= 0 && state.hovered_node < graph.node_count
        && !state.is_dragging && !state.lasso_active) {
        const GraphNode& n = graph.nodes[state.hovered_node];

        int degree = 0;
        for (int i = 0; i < graph.edge_count; ++i) {
            if ((int)graph.edges[i].source == state.hovered_node ||
                (int)graph.edges[i].target == state.hovered_node) {
                ++degree;
            }
        }

        ImGui::BeginTooltip();
        if (graph.types && n.type_id < (uint16_t)graph.type_count
                       && graph.types[n.type_id].name) {
            ImGui::Text("Type: %s", graph.types[n.type_id].name);
        } else {
            ImGui::Text("Type: %u", (unsigned)n.type_id);
        }
        ImGui::Text("Index: %d", state.hovered_node);
        if (n.user_data) ImGui::Text("user_data: %llu", (unsigned long long)n.user_data);
        ImGui::Text("Degree: %d", degree);
        ImGui::EndTooltip();
    }

    // -------------------------------------------------------------------
    // 9. Status bar overlay
    // -------------------------------------------------------------------
    {
        char status[160];
        snprintf(status, sizeof(status),
                 "Nodes: %d | Edges: %d | Sel: %zu | Zoom: %.2fx | Energy: %.4f | "
                 "[Space] layout [F] fit [Esc] clear [Shift+drag] lasso [Ctrl+click] toggle",
                 graph.node_count, graph.edge_count,
                 state.selection.size(), state.zoom, state.layout_energy);

        ImVec2 text_pos = ImVec2(widget_pos.x + 6.0f, widget_pos.y + h - 18.0f);
        draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 200), status);
    }

    ImGui::PopID();
    return interacted;
}
