#include "renderer.h"
#include "decimator.h"
#include "lttb.h"

#include "imgui.h"
#include "imgui_impl_glfw.h"
#ifdef DAQ_OPENGL3
#  include "imgui_impl_opengl3.h"
#else
#  include "imgui_impl_opengl2.h"
#endif
#include "implot.h"
#include <GLFW/glfw3.h>

#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>

// ─────────────────────────────────────────────────────────────────────
// Layout descriptors
// ─────────────────────────────────────────────────────────────────────

struct PanelRect { float x, y, w, h; };   // fractions of preview area

struct LayoutInfo {
    const char* label;
    Renderer::Layout layout;
    int              rows, cols;           // for sizing
    PanelRect        panels[4];
    int              npanels;
};

static const LayoutInfo kLayouts[] = {
    {"1×1", Renderer::Layout::L1x1, 1,1, {{0,0,1,1}}, 1},
    {"1×2", Renderer::Layout::L1x2, 1,2, {{0,0,.5f,1},{.5f,0,.5f,1}}, 2},
    {"1×3", Renderer::Layout::L1x3, 1,3, {{0,0,.333f,1},{.333f,0,.333f,1},{.666f,0,.334f,1}}, 3},
    {"1×4", Renderer::Layout::L1x4, 1,4, {{0,0,.25f,1},{.25f,0,.25f,1},{.5f,0,.25f,1},{.75f,0,.25f,1}}, 4},
    {"2×1", Renderer::Layout::L2x1, 2,1, {{0,0,1,.5f},{0,.5f,1,.5f}}, 2},
    {"2×2", Renderer::Layout::L2x2, 2,2, {{0,0,.5f,.5f},{.5f,0,.5f,.5f},{0,.5f,.5f,.5f},{.5f,.5f,.5f,.5f}}, 4},
    {"3×1", Renderer::Layout::L3x1, 3,1, {{0,0,1,.333f},{0,.333f,1,.333f},{0,.666f,1,.334f}}, 3},
    {"4×1", Renderer::Layout::L4x1, 4,1, {{0,0,1,.25f},{0,.25f,1,.25f},{0,.5f,1,.25f},{0,.75f,1,.25f}}, 4},
};
static constexpr int kNumLayouts = sizeof(kLayouts)/sizeof(kLayouts[0]);

// ─────────────────────────────────────────────────────────────────────
// Scratch buffers
// ─────────────────────────────────────────────────────────────────────

static constexpr size_t kMaxPlotPts = 2'000'000;
static constexpr size_t kMaxArchPts =   200'000;  // archive read limit per signal
static constexpr size_t kLTTBTarget =     4'000;  // LTTB output target (line mode)
static double g_tmp_t[kMaxPlotPts];
static double g_tmp_v[kMaxPlotPts];
static std::vector<double> s_lttb_t;
static std::vector<double> s_lttb_v;

// ─────────────────────────────────────────────────────────────────────
// Construction / destruction
// ─────────────────────────────────────────────────────────────────────

Renderer::Renderer(const AppConfig& cfg) : cfg_(cfg) {
    std::snprintf(new_label_, sizeof(new_label_), "Source");
    std::snprintf(new_host_,  sizeof(new_host_),  "localhost");
    trig_pre_sec_  = static_cast<float>(cfg_.trigger.pretrigger_sec);
    trig_post_sec_ = static_cast<float>(cfg_.trigger.posttrigger_sec);
}

Renderer::~Renderer() {
    {
        std::lock_guard<std::mutex> lk(sources_mutex_);
        for (auto& s : sources_) s->stop();
    }
    if (window_) {
        ImPlot::DestroyContext();
#ifdef DAQ_OPENGL3
        ImGui_ImplOpenGL3_Shutdown();
#else
        ImGui_ImplOpenGL2_Shutdown();
#endif
        ImGui_ImplGlfw_Shutdown();
        ImGui::DestroyContext();
        glfwDestroyWindow(window_);
        glfwTerminate();
    }
}

// ─────────────────────────────────────────────────────────────────────
// Style
// ─────────────────────────────────────────────────────────────────────

void Renderer::apply_dark_style() {
    ImGuiStyle& s = ImGui::GetStyle();
    s.WindowRounding     = 5.f;
    s.ChildRounding      = 4.f;
    s.FrameRounding      = 4.f;
    s.GrabRounding       = 3.f;
    s.TabRounding        = 4.f;
    s.ScrollbarRounding  = 3.f;
    s.PopupRounding      = 4.f;
    s.WindowBorderSize   = 0.f;
    s.FrameBorderSize    = 0.f;
    s.ChildBorderSize    = 1.f;
    s.ItemSpacing        = {6.f, 4.f};
    s.ItemInnerSpacing   = {4.f, 4.f};
    s.FramePadding       = {6.f, 3.f};
    s.WindowPadding      = {8.f, 6.f};
    s.ScrollbarSize      = 10.f;
    s.GrabMinSize        = 8.f;

    auto* c = s.Colors;
    c[ImGuiCol_WindowBg]          = {0.09f, 0.09f, 0.11f, 1.f};
    c[ImGuiCol_ChildBg]           = {0.11f, 0.11f, 0.14f, 1.f};
    c[ImGuiCol_PopupBg]           = {0.12f, 0.12f, 0.16f, 0.97f};
    c[ImGuiCol_Border]            = {0.22f, 0.22f, 0.30f, 0.7f};
    c[ImGuiCol_FrameBg]           = {0.16f, 0.16f, 0.21f, 1.f};
    c[ImGuiCol_FrameBgHovered]    = {0.20f, 0.24f, 0.32f, 1.f};
    c[ImGuiCol_FrameBgActive]     = {0.18f, 0.26f, 0.40f, 1.f};
    c[ImGuiCol_TitleBg]           = {0.07f, 0.07f, 0.09f, 1.f};
    c[ImGuiCol_TitleBgActive]     = {0.09f, 0.16f, 0.28f, 1.f};
    c[ImGuiCol_MenuBarBg]         = {0.09f, 0.09f, 0.12f, 1.f};
    c[ImGuiCol_ScrollbarBg]       = {0.06f, 0.06f, 0.08f, 1.f};
    c[ImGuiCol_ScrollbarGrab]     = {0.22f, 0.22f, 0.30f, 1.f};
    c[ImGuiCol_ScrollbarGrabHovered]={0.30f, 0.30f, 0.40f, 1.f};
    c[ImGuiCol_ScrollbarGrabActive] ={0.35f, 0.50f, 0.75f, 1.f};
    c[ImGuiCol_CheckMark]         = {0.35f, 0.65f, 1.00f, 1.f};
    c[ImGuiCol_SliderGrab]        = {0.30f, 0.52f, 0.82f, 1.f};
    c[ImGuiCol_SliderGrabActive]  = {0.38f, 0.62f, 0.95f, 1.f};
    c[ImGuiCol_Button]            = {0.18f, 0.18f, 0.24f, 1.f};
    c[ImGuiCol_ButtonHovered]     = {0.24f, 0.36f, 0.56f, 1.f};
    c[ImGuiCol_ButtonActive]      = {0.20f, 0.44f, 0.70f, 1.f};
    c[ImGuiCol_Header]            = {0.20f, 0.30f, 0.48f, 0.8f};
    c[ImGuiCol_HeaderHovered]     = {0.26f, 0.38f, 0.58f, 1.f};
    c[ImGuiCol_HeaderActive]      = {0.22f, 0.44f, 0.68f, 1.f};
    c[ImGuiCol_Separator]         = {0.20f, 0.20f, 0.28f, 1.f};
    c[ImGuiCol_SeparatorHovered]  = {0.30f, 0.44f, 0.65f, 1.f};
    c[ImGuiCol_SeparatorActive]   = {0.35f, 0.55f, 0.80f, 1.f};
    c[ImGuiCol_ResizeGrip]        = {0.25f, 0.40f, 0.65f, 0.4f};
    c[ImGuiCol_ResizeGripHovered] = {0.32f, 0.50f, 0.78f, 1.f};
    c[ImGuiCol_ResizeGripActive]  = {0.38f, 0.60f, 0.90f, 1.f};
    c[ImGuiCol_Tab]               = {0.14f, 0.14f, 0.19f, 1.f};
    c[ImGuiCol_TabHovered]        = {0.26f, 0.36f, 0.56f, 1.f};
    c[ImGuiCol_TabSelected]       = {0.20f, 0.32f, 0.52f, 1.f};
    c[ImGuiCol_Text]              = {0.88f, 0.88f, 0.92f, 1.f};
    c[ImGuiCol_TextDisabled]      = {0.38f, 0.40f, 0.48f, 1.f};
    c[ImGuiCol_PlotLines]         = {0.45f, 0.72f, 1.00f, 1.f};
    c[ImGuiCol_PlotHistogram]     = {0.30f, 0.55f, 0.85f, 1.f};
}

void Renderer::apply_implot_style() {
    ImPlotStyle& s = ImPlot::GetStyle();
    s.PlotPadding   = {8.f, 8.f};
    s.LabelPadding  = {4.f, 4.f};
    s.LegendPadding = {6.f, 6.f};
    s.PlotBorderSize= 1.f;
    s.Colors[ImPlotCol_PlotBg]     = ImVec4(0.06f, 0.06f, 0.08f, 1.f);
    s.Colors[ImPlotCol_PlotBorder] = ImVec4(0.22f, 0.22f, 0.30f, 0.8f);
    s.Colors[ImPlotCol_AxisGrid]   = ImVec4(0.20f, 0.20f, 0.28f, 0.6f);
    s.Colors[ImPlotCol_AxisText]   = ImVec4(0.55f, 0.57f, 0.65f, 1.f);
    s.Colors[ImPlotCol_LegendBg]   = ImVec4(0.10f, 0.10f, 0.14f, 0.90f);
    s.Colors[ImPlotCol_LegendBorder]= ImVec4(0.22f, 0.22f, 0.30f, 0.7f);
    s.Colors[ImPlotCol_LegendText] = ImVec4(0.85f, 0.85f, 0.90f, 1.f);
    s.Colors[ImPlotCol_Crosshairs] = ImVec4(0.45f, 0.48f, 0.60f, 0.6f);
}

void Renderer::load_fonts() {
    ImGuiIO& io = ImGui::GetIO();
    io.Fonts->Clear();

    // Try to load Inter (UI font)
    const char* inter_path = "/usr/share/fonts/inter/InterVariable.ttf";
    const char* mono_path  = "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf";
    // Fallback monospace path
    const char* mono_path2 = "/home/martino/.local/share/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Regular.ttf";

    ImFontConfig fc;
    fc.OversampleH = 3;
    fc.OversampleV = 2;
    fc.PixelSnapH  = false;

    ImFont* f = io.Fonts->AddFontFromFileTTF(inter_path, 14.f, &fc);
    font_ui_ = f ? f : io.Fonts->AddFontDefault();

    // Monospace font for values
    ImFont* fm = io.Fonts->AddFontFromFileTTF(mono_path, 13.f, &fc);
    if (!fm) fm = io.Fonts->AddFontFromFileTTF(mono_path2, 13.f, &fc);
    font_mono_ = fm ? fm : font_ui_;
    // Atlas is built automatically by the backend on first frame
}

// ─────────────────────────────────────────────────────────────────────
// Init
// ─────────────────────────────────────────────────────────────────────

bool Renderer::init() {
    if (!glfwInit()) {
        std::fprintf(stderr, "[renderer] glfwInit failed\n");
        return false;
    }
    int gl_major = cfg_.display.opengl_version;
    int gl_minor = (gl_major >= 3) ? 2 : 1;
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, gl_major);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, gl_minor);
    if (gl_major >= 3)
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    window_ = glfwCreateWindow(cfg_.display.width, cfg_.display.height,
                               "MARTe2 DAQ Viewer", nullptr, nullptr);
    if (!window_) {
        std::fprintf(stderr, "[renderer] glfwCreateWindow failed\n");
        glfwTerminate();
        return false;
    }
    glfwMakeContextCurrent(window_);
    glfwSwapInterval(1);

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    io.IniFilename = nullptr;

    apply_dark_style();
    load_fonts();

#ifdef DAQ_OPENGL3
    ImGui_ImplGlfw_InitForOpenGL(window_, true);
    ImGui_ImplOpenGL3_Init("#version 130");
#else
    ImGui_ImplGlfw_InitForOpenGL(window_, true);
    ImGui_ImplOpenGL2_Init();
#endif
    ImPlot::CreateContext();
    apply_implot_style();

    for (auto& sc : cfg_.sources)
        add_source(sc.label, sc.host, sc.port,
                   sc.multicast_group, sc.data_port);

    return true;
}

void Renderer::add_source(const std::string& label,
                           const std::string& host, uint16_t port,
                           const std::string& multicast_group, uint16_t data_port)
{
    auto src = std::make_shared<Source>(label, host, port, multicast_group, data_port);
    src->start();
    std::lock_guard<std::mutex> lk(sources_mutex_);
    sources_.push_back(std::move(src));
}

// ─────────────────────────────────────────────────────────────────────
// Run loop
// ─────────────────────────────────────────────────────────────────────

void Renderer::run() {
    using clock = std::chrono::steady_clock;
    auto frame_dur = std::chrono::microseconds(1000000 / cfg_.display.fps);
    auto next_frame = clock::now();
    while (!glfwWindowShouldClose(window_)) {
        glfwPollEvents();
        if (clock::now() < next_frame)
            std::this_thread::sleep_until(next_frame);
        next_frame = clock::now() + frame_dur;
        render_frame();
    }
}

// ─────────────────────────────────────────────────────────────────────
// render_frame — top-level layout
// ─────────────────────────────────────────────────────────────────────

void Renderer::render_frame() {
    glfwMakeContextCurrent(window_);
#ifdef DAQ_OPENGL3
    ImGui_ImplOpenGL3_NewFrame();
#else
    ImGui_ImplOpenGL2_NewFrame();
#endif
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    if (font_ui_) ImGui::PushFont(font_ui_, font_ui_->LegacySize);

    ImGuiIO& io = ImGui::GetIO();
    ImGui::SetNextWindowPos({0,0});
    ImGui::SetNextWindowSize(io.DisplaySize);
    const ImGuiWindowFlags host_flags =
        ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
        ImGuiWindowFlags_NoBringToFrontOnFocus;

    ImGui::Begin("##host", nullptr, host_flags);

    draw_toolbar();

    float stats_h   = show_stats_   ? 90.f  : 0.f;
    float trigger_h = show_trigger_ ? 115.f : 0.f;
    float bottom_h  = stats_h + trigger_h + (stats_h > 0 || trigger_h > 0 ? 4.f : 0.f);
    float content_h = io.DisplaySize.y - ImGui::GetCursorPosY() - bottom_h - 6.f;

    // ── Sidebar ───────────────────────────────────────────────────────
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4.f, 4.f});
    ImGui::BeginChild("##sidebar",
                      ImVec2(sidebar_w_, content_h),
                      ImGuiChildFlags_Borders);
    ImGui::PopStyleVar();
    draw_sidebar();
    ImGui::EndChild();

    // ── Drag splitter ─────────────────────────────────────────────────
    ImGui::SameLine(0, 0);
    ImGui::InvisibleButton("##vsplit", ImVec2(5.f, content_h));
    if (ImGui::IsItemHovered() || ImGui::IsItemActive())
        ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
    if (ImGui::IsItemActive()) {
        sidebar_w_ += io.MouseDelta.x;
        sidebar_w_  = std::clamp(sidebar_w_, 150.f, 520.f);
    }
    {   // Draw a subtle divider line
        ImVec2 rmin = ImGui::GetItemRectMin();
        ImVec2 rmax = ImGui::GetItemRectMax();
        float  cx   = (rmin.x + rmax.x) * 0.5f;
        ImU32  col  = ImGui::IsItemHovered() || ImGui::IsItemActive()
                      ? IM_COL32(90, 130, 200, 200)
                      : IM_COL32(50, 50, 70, 150);
        ImGui::GetWindowDrawList()->AddLine(
            {cx, rmin.y}, {cx, rmax.y}, col, 2.f);
    }

    // ── Plot area ─────────────────────────────────────────────────────
    ImGui::SameLine(0, 0);
    float plot_w = io.DisplaySize.x - sidebar_w_ - 5.f;
    ImGui::BeginChild("##plots", ImVec2(plot_w, content_h), ImGuiChildFlags_None);
    draw_plots(plot_w - 2.f, content_h - 2.f);
    ImGui::EndChild();

    // ── Bottom panels ─────────────────────────────────────────────────
    if (show_stats_)   draw_stats_panel();
    if (show_trigger_) draw_trigger_panel();

    // ── Popups (must be at top-level window scope) ─────────────────────
    if (open_add_source_modal_) {
        ImGui::OpenPopup("AddSourceModal");
        open_add_source_modal_ = false;
    }
    draw_add_source_modal();
    draw_signal_style_popup();

    ImGui::End();

    if (font_ui_) ImGui::PopFont();

    // Trigger logic
    poll_trigger();

    // Live time tracking
    if (live_mode_ && !paused_ && !trig_show_capture_) {
        double t = compute_latest_time();
        if (t > 0.0) view_t_max_ = t;
    }

    // Render
    ImGui::Render();
    int fw, fh;
    glfwGetFramebufferSize(window_, &fw, &fh);
    glViewport(0, 0, fw, fh);
    glClearColor(0.06f, 0.06f, 0.08f, 1.f);
    glClear(GL_COLOR_BUFFER_BIT);
#ifdef DAQ_OPENGL3
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
#else
    ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData());
#endif
    glfwSwapBuffers(window_);
}

// ─────────────────────────────────────────────────────────────────────
// Layout popup (button → popup with visual grid previews)
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_layout_popup() {
    // Find current layout label
    const char* cur_label = "1×1";
    for (int i = 0; i < kNumLayouts; ++i)
        if (kLayouts[i].layout == layout_) { cur_label = kLayouts[i].label; break; }

    char btn_label[32];
    std::snprintf(btn_label, sizeof(btn_label), "Layout: %s [v]", cur_label);
    if (ImGui::Button(btn_label))
        ImGui::OpenPopup("##layout_popup");

    if (!ImGui::BeginPopup("##layout_popup")) return;

    const float preview_w = 72.f, preview_h = 44.f;
    const float gap       = 6.f;
    // Draw 4 per row
    for (int i = 0; i < kNumLayouts; ++i) {
        const auto& li = kLayouts[i];
        bool selected  = (layout_ == li.layout);

        ImGui::PushID(i);
        if (i % 4 != 0) ImGui::SameLine(0, gap);

        ImVec2 p = ImGui::GetCursorScreenPos();
        // Invisible button sized to the preview
        bool clicked = ImGui::InvisibleButton("##lbtn",
                            ImVec2(preview_w, preview_h + 16.f));
        bool hovered = ImGui::IsItemHovered();

        // Draw preview box
        auto* dl = ImGui::GetWindowDrawList();
        ImU32 bg_col = selected   ? IM_COL32(40, 90, 180, 220)
                     : hovered    ? IM_COL32(30, 70, 140, 180)
                                  : IM_COL32(22, 22, 30, 220);
        dl->AddRectFilled(p, {p.x + preview_w, p.y + preview_h}, bg_col, 4.f);
        dl->AddRect(p, {p.x + preview_w, p.y + preview_h},
                    selected ? IM_COL32(100,170,255,255)
                             : IM_COL32(60, 60, 90, 200), 4.f);
        for (int j = 0; j < li.npanels; ++j) {
            const auto& pr = li.panels[j];
            ImVec2 p0 = {p.x + pr.x * preview_w + 2.f, p.y + pr.y * preview_h + 2.f};
            ImVec2 p1 = {p.x + (pr.x+pr.w)*preview_w - 2.f, p.y+(pr.y+pr.h)*preview_h - 2.f};
            dl->AddRectFilled(p0, p1, IM_COL32(50, 110, 200, 180), 2.f);
            dl->AddRect(p0, p1, IM_COL32(110, 170, 255, 200), 2.f);
        }
        // Label below preview
        ImVec2 lp = {p.x, p.y + preview_h + 2.f};
        dl->AddText(lp, IM_COL32(200, 205, 215, 255), li.label);

        if (clicked) {
            layout_ = li.layout;
            ImGui::CloseCurrentPopup();
        }
        ImGui::PopID();
    }
    ImGui::EndPopup();
}

// ─────────────────────────────────────────────────────────────────────
// Toolbar
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_toolbar() {
    const float h = ImGui::GetFrameHeight();

    // ── Pause / Resume ───────────────────────────────────────────────
    bool was_paused = paused_;
    if (was_paused) {
        ImGui::PushStyleColor(ImGuiCol_Button,
            ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
    }
    if (ImGui::Button(paused_ ? "  Resume  " : "  Pause  ")) {
        paused_ = !paused_;
        if (!paused_) live_mode_ = true;
    }
    if (was_paused) ImGui::PopStyleColor();

    ImGui::SameLine(0, 10.f);
    // ── Window size ──────────────────────────────────────────────────
    ImGui::TextDisabled("Window:");
    ImGui::SameLine();
    static const double kWins[]      = {1,2,5,10,30,60};
    static const char*  kWinLabels[] = {"1s","2s","5s","10s","30s","60s"};
    static int win_sel = 2;
    ImGui::SetNextItemWidth(60.f);
    if (ImGui::Combo("##win", &win_sel, kWinLabels, 6))
        window_sec_ = kWins[win_sel];

    ImGui::SameLine(0, 10.f);
    // ── Layout popup ─────────────────────────────────────────────────
    draw_layout_popup();

    ImGui::SameLine(0, 10.f);
    // ── Dual-cursor toggles ──────────────────────────────────────────
    ImGui::TextDisabled("Cur:");
    ImGui::SameLine(0, 3.f);
    {
        bool a = cursor_a_active_;
        if (a) ImGui::PushStyleColor(ImGuiCol_Button,
                    ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
        if (ImGui::Button("A", {h, h})) cursor_a_active_ = !cursor_a_active_;
        if (a) ImGui::PopStyleColor();
    }
    ImGui::SameLine(0, 2.f);
    {
        bool b = cursor_b_active_;
        if (b) ImGui::PushStyleColor(ImGuiCol_Button,
                    ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
        if (ImGui::Button("B", {h, h})) cursor_b_active_ = !cursor_b_active_;
        if (b) ImGui::PopStyleColor();
    }
    if (cursor_a_active_ && cursor_b_active_) {
        ImGui::SameLine(0, 6.f);
        if (font_mono_) ImGui::PushFont(font_mono_, font_mono_->LegacySize);
        ImGui::TextDisabled("ΔT %+.4f s", cursor_b_time_ - cursor_a_time_);
        if (font_mono_) ImGui::PopFont();
    }

    // ── Trigger / Stats toggles + Add Source ────────────────────────
    ImGui::SameLine(0, 10.f);
    {
        bool t = show_trigger_;
        if (t) ImGui::PushStyleColor(ImGuiCol_Button,
                    ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
        if (ImGui::Button("Trigger")) show_trigger_ = !show_trigger_;
        if (t) ImGui::PopStyleColor();
    }
    ImGui::SameLine(0, 4.f);
    {
        bool s = show_stats_;
        if (s) ImGui::PushStyleColor(ImGuiCol_Button,
                    ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
        if (ImGui::Button("Stats")) show_stats_ = !show_stats_;
        if (s) ImGui::PopStyleColor();
    }
    ImGui::SameLine(0, 4.f);
    if (ImGui::Button("+ Source"))
        open_add_source_modal_ = true;

    // ── FPS (right-aligned) ──────────────────────────────────────────
    ImGui::SameLine(ImGui::GetWindowWidth() - 75.f);
    if (font_mono_) ImGui::PushFont(font_mono_, font_mono_->LegacySize);
    ImGui::TextDisabled("%.0f FPS", ImGui::GetIO().Framerate);
    if (font_mono_) ImGui::PopFont();

    ImGui::Separator();
}

// ─────────────────────────────────────────────────────────────────────
// Sidebar
// ─────────────────────────────────────────────────────────────────────

struct DragSigPayload { int src_idx; int sig_idx; };

void Renderer::draw_sidebar() {
    ImGui::Spacing();
    ImGui::TextDisabled("Drag signals onto plots");
    ImGui::Separator();

    std::lock_guard<std::mutex> lk(sources_mutex_);

    for (int si = 0; si < static_cast<int>(sources_.size()); ++si) {
        auto& src = sources_[si];
        ImGui::PushID(si);

        // Connection dot + source header
        ImGui::PushStyleColor(ImGuiCol_Text,
            src->connected() ? ImVec4{0.3f,0.9f,0.4f,1.f}
                              : ImVec4{0.7f,0.3f,0.3f,1.f});
        ImGui::TextUnformatted("●");
        ImGui::PopStyleColor();
        ImGui::SameLine();

        char hdr[80];
        std::snprintf(hdr, sizeof(hdr), "%s##h%d", src->label().c_str(), si);
        bool open = ImGui::CollapsingHeader(hdr, ImGuiTreeNodeFlags_DefaultOpen);

        if (open) {
            ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{0.42f, 0.45f, 0.55f, 1.f});
            ImGui::TextUnformatted(src->host().c_str());
            ImGui::PopStyleColor();

            std::lock_guard<std::mutex> slk(src->signals_mutex);
            if (src->signal_states.empty()) {
                ImGui::TextDisabled("  waiting for CONFIG...");
            } else {
                for (int gi = 0; gi < static_cast<int>(src->signal_states.size()); ++gi) {
                    auto& sig = src->signal_states[gi];
                    ImGui::PushID(gi);

                    // Colour swatch — opens style popup
                    ImVec4 col(sig.color[0], sig.color[1], sig.color[2], sig.color[3]);
                    if (ImGui::ColorButton("##col", col,
                            ImGuiColorEditFlags_NoTooltip, {12.f, 12.f})) {
                        style_src_idx_ = si;
                        style_sig_idx_ = gi;
                        ImGui::OpenPopup("SignalStylePopup");
                    }
                    ImGui::SameLine(0, 4.f);

                    // Visibility toggle
                    ImGui::Checkbox("##v", &sig.visible);
                    ImGui::SameLine(0, 4.f);

                    // Name — drag source
                    // Dim if not shown on any plot
                    bool in_plot = sig.plot_mask != 0;
                    if (!in_plot)
                        ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{0.55f,0.55f,0.60f,1.f});
                    ImGui::TextUnformatted(sig.def.name.c_str());
                    if (!in_plot) ImGui::PopStyleColor();

                    // Make the name a drag source
                    if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
                        DragSigPayload pl{si, gi};
                        ImGui::SetDragDropPayload("SIGNAL", &pl, sizeof(pl));
                        ImGui::PushStyleColor(ImGuiCol_Text,
                            ImVec4(sig.color[0], sig.color[1], sig.color[2], 1.f));
                        ImGui::TextUnformatted(sig.def.name.c_str());
                        ImGui::PopStyleColor();
                        ImGui::TextDisabled("Drop onto a plot");
                        ImGui::EndDragDropSource();
                    }

                    if (!sig.def.unit.empty()) {
                        ImGui::SameLine(0, 3.f);
                        ImGui::TextDisabled("[%s]", sig.def.unit.c_str());
                    }

                    // Plot badges (one per active bit) + per-plot remove buttons
                    for (int pi = 0; pi < 4; ++pi) {
                        if (!(sig.plot_mask & (1u << pi))) continue;
                        ImGui::SameLine(0, pi == 0 ? 4.f : 1.f);
                        ImGui::PushID(pi);
                        ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{0.5f,0.75f,1.f,1.f});
                        ImGui::TextDisabled("P%d", pi);
                        ImGui::PopStyleColor();
                        ImGui::SameLine(0, 1.f);
                        ImGui::PushStyleColor(ImGuiCol_Button,        ImVec4{0,0,0,0});
                        ImGui::PushStyleColor(ImGuiCol_ButtonHovered,  ImVec4{0.7f,0.2f,0.2f,0.7f});
                        ImGui::PushStyleColor(ImGuiCol_Text,           ImVec4{0.7f,0.4f,0.4f,1.f});
                        if (ImGui::SmallButton("x"))
                            sig.plot_mask &= ~static_cast<uint8_t>(1u << pi);
                        ImGui::PopStyleColor(3);
                        if (ImGui::IsItemHovered())
                            ImGui::SetTooltip("Remove from P%d", pi);
                        ImGui::PopID();
                    }

                    ImGui::PopID();
                }
            }
        }

        ImGui::PopID();
        ImGui::Spacing();
    }
}

// ─────────────────────────────────────────────────────────────────────
// Plot grid
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_plots(float w, float h) {
    float hw = w * 0.5f, hh = h * 0.5f;
    float w3 = w * 0.3333f, h3 = h * 0.3333f;
    float w4 = w * 0.25f,   h4 = h * 0.25f;

    switch (layout_) {
    case Layout::L1x1:
        draw_single_plot(0, w, h);
        break;
    case Layout::L1x2:
        draw_single_plot(0, hw, h); ImGui::SameLine(0,0);
        draw_single_plot(1, hw, h);
        break;
    case Layout::L1x3:
        draw_single_plot(0, w3, h); ImGui::SameLine(0,0);
        draw_single_plot(1, w3, h); ImGui::SameLine(0,0);
        draw_single_plot(2, w3, h);
        break;
    case Layout::L1x4:
        draw_single_plot(0, w4, h); ImGui::SameLine(0,0);
        draw_single_plot(1, w4, h); ImGui::SameLine(0,0);
        draw_single_plot(2, w4, h); ImGui::SameLine(0,0);
        draw_single_plot(3, w4, h);
        break;
    case Layout::L2x1:
        draw_single_plot(0, w, hh);
        draw_single_plot(1, w, hh);
        break;
    case Layout::L2x2:
        draw_single_plot(0, hw, hh); ImGui::SameLine(0,0);
        draw_single_plot(1, hw, hh);
        draw_single_plot(2, hw, hh); ImGui::SameLine(0,0);
        draw_single_plot(3, hw, hh);
        break;
    case Layout::L3x1:
        draw_single_plot(0, w, h3);
        draw_single_plot(1, w, h3);
        draw_single_plot(2, w, h3);
        break;
    case Layout::L4x1:
        draw_single_plot(0, w, h4);
        draw_single_plot(1, w, h4);
        draw_single_plot(2, w, h4);
        draw_single_plot(3, w, h4);
        break;
    }
}

void Renderer::draw_single_plot(int plot_id, float w, float h) {
    char name[32];
    std::snprintf(name, sizeof(name), "##plot%d", plot_id);

    double t_min = view_t_max_ - window_sec_;
    double t_max = view_t_max_;

    bool trig_mode = trig_show_capture_ && !cap_t_.empty() && (plot_id == 0);

    if (!ImPlot::BeginPlot(name, ImVec2(w, h), ImPlotFlags_NoFrame)) return;

    ImPlot::SetupAxis(ImAxis_X1,
                      trig_mode ? "Time rel. trigger [s]" : "Time [s]");
    ImPlot::SetupAxis(ImAxis_Y1, nullptr, ImPlotAxisFlags_AutoFit);

    if (trig_mode) {
        ImPlot::SetupAxisLimits(ImAxis_X1,
            -(double)trig_pre_sec_, (double)trig_post_sec_, ImPlotCond_Always);
    } else {
        ImPlot::SetupAxisLimits(ImAxis_X1, t_min, t_max,
            (live_mode_ && !paused_) ? ImPlotCond_Always : ImPlotCond_Once);
        if (paused_ || !live_mode_) {
            auto lims  = ImPlot::GetPlotLimits();
            view_t_max_= lims.X.Max;
        }
    }

    int cols = std::max(static_cast<int>(ImPlot::GetPlotSize().x) * 2, 400);

    if (trig_mode) {
        double trig_t = trig_engine_.trigger_time();
        size_t n = cap_t_.size();
        static std::vector<double> rel_t;
        rel_t.resize(n);
        for (size_t i = 0; i < n; ++i) rel_t[i] = cap_t_[i] - trig_t;

        std::string sig_name = "capture";
        {
            std::lock_guard<std::mutex> lk(sources_mutex_);
            if (trig_src_idx_ < static_cast<int>(sources_.size())) {
                auto& src = sources_[trig_src_idx_];
                std::lock_guard<std::mutex> slk(src->signals_mutex);
                if (trig_sig_idx_ < static_cast<int>(src->signal_states.size()))
                    sig_name = src->signal_states[trig_sig_idx_].def.name;
            }
        }
        {
            ImPlotSpec sp; sp.LineColor = ImVec4(1.f, 0.75f, 0.1f, 1.f); sp.LineWeight = 1.5f;
            ImPlot::PlotLine(sig_name.c_str(), rel_t.data(), cap_v_.data(),
                             static_cast<int>(n), sp);
        }
        // t=0 marker
        {
            ImPlotSpec sp; sp.LineColor = ImVec4(0.9f, 0.2f, 0.2f, 0.7f); sp.LineWeight = 1.f;
            double xt[2] = {0.0, 0.0}, yt[2] = {-1e9, 1e9};
            ImPlot::PlotLine("##t0", xt, yt, 2, sp);
        }
        // threshold marker
        {
            ImPlotSpec sp; sp.LineColor = ImVec4(0.9f, 0.6f, 0.1f, 0.5f); sp.LineWeight = 1.f;
            double xt[2] = {-(double)trig_pre_sec_, (double)trig_post_sec_};
            double yt[2] = {trig_engine_.threshold(), trig_engine_.threshold()};
            ImPlot::PlotLine("##thr", xt, yt, 2, sp);
        }
        ImPlot::EndPlot();
        return;
    }

    // Normal live/paused view
    bool any_plotted = false;
    {
        std::lock_guard<std::mutex> lk(sources_mutex_);
        for (auto& src : sources_) {
            std::lock_guard<std::mutex> slk(src->signals_mutex);
            for (auto& sig : src->signal_states) {
                if (!sig.visible || !(sig.plot_mask & (1u << plot_id)) || !sig.buffer) continue;

                // ── Select data source (G: archive fallback for long history) ───
                // Archive is used ONLY when the buffer has zero coverage of the
                // current window — i.e. the user has scrolled back far enough that
                // the ring buffer cycled past.  For a mixed window (archive + buffer
                // overlap), only the buffer is shown; combining them would give LTTB
                // hugely skewed sample-count buckets and corrupt the visible signal.
                double buf_oldest = sig.buffer->oldest_time();
                bool archive_only = sig.archive &&
                    (sig.buffer->count() == 0 || buf_oldest >= t_max);
                size_t n = archive_only
                    ? sig.archive->read_window(t_min, t_max, g_tmp_t, g_tmp_v, kMaxPlotPts)
                    : sig.buffer ->read_window(t_min, t_max, g_tmp_t, g_tmp_v, kMaxPlotPts);
                if (n == 0) continue;
                any_plotted = true;

                ImPlotSpec sp;
                sp.LineColor  = ImVec4(sig.color[0], sig.color[1],
                                       sig.color[2], sig.color[3]);
                sp.LineWeight = sig.line_width;

                // ── Rendering strategy ────────────────────────────────────────────
                // Very dense pure-buffer data: min/max envelope preserves peaks.
                // Everything else (sparse, archive, or moderate): LTTB for visual fidelity.
                if (!archive_only && n > static_cast<size_t>(cols) * 4) {
                    // Envelope (PlotShaded) — shows true signal peaks for dense data
                    auto env = Decimator::decimate(g_tmp_t, g_tmp_v, n, cols, t_min, t_max);
                    if (!env.t.empty()) {
                        ImPlotSpec sps = sp;
                        sps.FillColor = ImVec4(sig.color[0], sig.color[1],
                                               sig.color[2], 0.25f);
                        ImPlot::PlotShaded(sig.def.name.c_str(),
                                           env.t.data(), env.vmin.data(), env.vmax.data(),
                                           static_cast<int>(env.t.size()), sps);
                    }
                } else if (n > kLTTBTarget) {
                    // LTTB — visually optimal downsampling (A: replaces crude midpoint)
                    s_lttb_t.resize(kLTTBTarget);
                    s_lttb_v.resize(kLTTBTarget);
                    size_t m = lttb(g_tmp_t, g_tmp_v, n,
                                    s_lttb_t.data(), s_lttb_v.data(), kLTTBTarget);
                    ImPlot::PlotLine(sig.def.name.c_str(),
                                     s_lttb_t.data(), s_lttb_v.data(),
                                     static_cast<int>(m), sp);
                } else {
                    // Sparse / zoomed in: plot all points directly
                    ImPlot::PlotLine(sig.def.name.c_str(),
                                     g_tmp_t, g_tmp_v, static_cast<int>(n), sp);
                }
            }
        }
    }

    // "Drop here" hint when plot is empty
    if (!any_plotted) {
        ImPlot::PushStyleColor(ImPlotCol_InlayText, ImVec4(0.4f,0.42f,0.5f,0.8f));
        ImPlot::PlotText("Drop signals here", 0, 0);
        ImPlot::PopStyleColor();
    }

    // Cursor A  (Ctrl+LClick to place)
    if (cursor_a_active_) {
        ImPlotSpec sp; sp.LineColor = ImVec4(1.f,1.f,0.f,0.75f); sp.LineWeight = 1.5f;
        double ta[2] = {cursor_a_time_, cursor_a_time_};
        double va[2] = {-1e15, 1e15};
        ImPlot::PlotLine("##ca", ta, va, 2, sp);
        if (ImPlot::IsPlotHovered() &&
                ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
                ImGui::GetIO().KeyCtrl)
            cursor_a_time_ = ImPlot::GetPlotMousePos().x;
    }
    // Cursor B  (Ctrl+RClick to place)
    if (cursor_b_active_) {
        ImPlotSpec sp; sp.LineColor = ImVec4(0.f,1.f,1.f,0.75f); sp.LineWeight = 1.5f;
        double tb[2] = {cursor_b_time_, cursor_b_time_};
        double vb[2] = {-1e15, 1e15};
        ImPlot::PlotLine("##cb", tb, vb, 2, sp);
        if (ImPlot::IsPlotHovered() &&
                ImGui::IsMouseClicked(ImGuiMouseButton_Right) &&
                ImGui::GetIO().KeyCtrl)
            cursor_b_time_ = ImPlot::GetPlotMousePos().x;
    }

    ImPlot::EndPlot();

    // Drop target: accept SIGNAL payloads dropped onto this plot
    if (ImGui::BeginDragDropTarget()) {
        if (const ImGuiPayload* payload =
                ImGui::AcceptDragDropPayload("SIGNAL")) {
            auto* pl = static_cast<const DragSigPayload*>(payload->Data);
            std::lock_guard<std::mutex> lk(sources_mutex_);
            if (pl->src_idx < static_cast<int>(sources_.size())) {
                auto& src = sources_[pl->src_idx];
                std::lock_guard<std::mutex> slk(src->signals_mutex);
                if (pl->sig_idx < static_cast<int>(src->signal_states.size()))
                    src->signal_states[pl->sig_idx].plot_mask |= static_cast<uint8_t>(1u << plot_id);
            }
        }
        ImGui::EndDragDropTarget();
    }
}

// ─────────────────────────────────────────────────────────────────────
// Trigger panel
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_trigger_panel() {
    ImGui::Separator();
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6.f, 4.f});
    ImGui::BeginChild("##trig_panel", ImVec2(-1, 0), ImGuiChildFlags_Borders);
    ImGui::PopStyleVar();

    auto arm_trigger = [&]() {
        trig_engine_.configure(
            static_cast<TriggerEngine::Edge>(trig_edge_sel_),
            static_cast<double>(trig_threshold_),
            static_cast<TriggerEngine::Mode>(trig_mode_sel_),
            static_cast<double>(trig_pre_sec_),
            static_cast<double>(trig_post_sec_));
        trig_engine_.arm();
        trig_show_capture_ = false;
        trig_last_written_ = 0;
    };

    // ── Row 1: Source / Signal / Edge / Threshold ─────────────────────
    {
        std::lock_guard<std::mutex> lk(sources_mutex_);

        if (!sources_.empty()) {
            std::vector<const char*> snames;
            for (auto& s : sources_) snames.push_back(s->label().c_str());

            ImGui::TextDisabled("Source");  ImGui::SameLine();
            ImGui::SetNextItemWidth(110.f);
            ImGui::Combo("##ts", &trig_src_idx_, snames.data(),
                         static_cast<int>(snames.size()));
            ImGui::SameLine(0, 10.f);

            if (trig_src_idx_ < static_cast<int>(sources_.size())) {
                auto& src = sources_[trig_src_idx_];
                std::lock_guard<std::mutex> slk(src->signals_mutex);
                if (!src->signal_states.empty()) {
                    std::vector<const char*> sgnames;
                    for (auto& sg : src->signal_states)
                        sgnames.push_back(sg.def.name.c_str());
                    ImGui::TextDisabled("Signal");  ImGui::SameLine();
                    ImGui::SetNextItemWidth(130.f);
                    ImGui::Combo("##tsg", &trig_sig_idx_,
                                 sgnames.data(), static_cast<int>(sgnames.size()));
                    ImGui::SameLine(0, 10.f);
                }
            }
        }
    }

    static const char* kEdges[] = {"Rising ↑", "Falling ↓", "Either ↕"};
    ImGui::TextDisabled("Edge");  ImGui::SameLine();
    ImGui::SetNextItemWidth(85.f);
    ImGui::Combo("##te", &trig_edge_sel_, kEdges, 3);
    ImGui::SameLine(0, 10.f);
    ImGui::TextDisabled("Threshold");  ImGui::SameLine();
    ImGui::SetNextItemWidth(90.f);
    if (font_mono_) ImGui::PushFont(font_mono_, font_mono_->LegacySize);
    ImGui::InputFloat("##tthr", &trig_threshold_, 0.f, 0.f, "%.5f");
    if (font_mono_) ImGui::PopFont();

    // ── Row 2: Pre / Post / Mode / Arm ───────────────────────────────
    ImGui::TextDisabled("Pre");  ImGui::SameLine();
    ImGui::SetNextItemWidth(70.f);
    ImGui::InputFloat("s##tp", &trig_pre_sec_,  0.f, 0.f, "%.3f");
    ImGui::SameLine(0, 10.f);
    ImGui::SetNextItemWidth(70.f);
    ImGui::InputFloat("s##tpo", &trig_post_sec_, 0.f, 0.f, "%.3f");
    ImGui::SameLine(0, 16.f);

    // Mode radio buttons
    ImGui::TextDisabled("Mode");  ImGui::SameLine();
    ImGui::RadioButton("Single", &trig_mode_sel_, 0);
    ImGui::SameLine();
    ImGui::RadioButton("Normal", &trig_mode_sel_, 1);
    ImGui::SameLine(0, 16.f);

    // Arm / status
    auto st      = trig_engine_.state();
    bool captured= trig_show_capture_;

    if (st == TriggerEngine::State::Idle && !captured) {
        if (ImGui::Button("  Arm  ")) arm_trigger();
    } else {
        if (ImGui::Button("Disarm")) {
            trig_engine_.disarm();
            trig_show_capture_ = false;
        }
        ImGui::SameLine(0, 8.f);
        if      (st == TriggerEngine::State::Armed)
            ImGui::TextColored({0.3f,0.9f,0.4f,1.f}, "ARMED");
        else if (st == TriggerEngine::State::Fired)
            ImGui::TextColored({1.f,0.6f,0.1f,1.f}, "COLLECTING...");
        else if (st == TriggerEngine::State::Idle && captured) {
            ImGui::TextColored({1.f,0.9f,0.1f,1.f}, "TRIGGERED");
            ImGui::SameLine(0, 8.f);
            if (ImGui::Button("Rearm")) arm_trigger();
        }
    }

    ImGui::EndChild();
}

// ─────────────────────────────────────────────────────────────────────
// Trigger polling (render thread)
// ─────────────────────────────────────────────────────────────────────

void Renderer::poll_trigger() {
    auto st = trig_engine_.state();
    if (st == TriggerEngine::State::Idle) return;

    std::shared_ptr<Source> src;
    {
        std::lock_guard<std::mutex> lk(sources_mutex_);
        if (trig_src_idx_ >= static_cast<int>(sources_.size())) return;
        src = sources_[trig_src_idx_];
    }

    std::lock_guard<std::mutex> slk(src->signals_mutex);
    if (trig_sig_idx_ >= static_cast<int>(src->signal_states.size())) return;
    auto& sig = src->signal_states[trig_sig_idx_];
    if (!sig.buffer) return;

    size_t total = sig.buffer->total_written();
    if (total <= trig_last_written_) return;
    size_t new_count = std::min(total - trig_last_written_, kMaxPlotPts);
    size_t got       = sig.buffer->read_latest(new_count, g_tmp_t, g_tmp_v);
    trig_last_written_ = total;

    if (st == TriggerEngine::State::Armed)
        trig_engine_.feed_new(g_tmp_t, g_tmp_v, got);

    if (trig_engine_.state() == TriggerEngine::State::Fired) {
        double latest = sig.buffer->latest_time();
        if (trig_engine_.post_complete(latest)) {
            double ttime = trig_engine_.trigger_time();
            double pre   = static_cast<double>(trig_pre_sec_);
            double post  = static_cast<double>(trig_post_sec_);
            size_t n = sig.buffer->read_window(
                ttime - pre, ttime + post, g_tmp_t, g_tmp_v, kMaxPlotPts);
            cap_t_.assign(g_tmp_t, g_tmp_t + n);
            cap_v_.assign(g_tmp_v, g_tmp_v + n);

            if (trig_mode_sel_ == 1) {   // Normal: re-arm immediately
                trig_show_capture_ = false;
                trig_engine_.configure(trig_engine_.edge(), trig_engine_.threshold(),
                    TriggerEngine::Mode::Normal, pre, post);
                trig_engine_.arm();
                trig_last_written_ = sig.buffer->total_written();
            } else {                     // Single: freeze
                trig_show_capture_ = true;
                trig_engine_.disarm();
            }
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// Stats panel
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_stats_panel() {
    ImGui::Separator();
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6.f, 4.f});
    ImGui::BeginChild("##stats_panel", ImVec2(-1, 90.f), ImGuiChildFlags_Borders);
    ImGui::PopStyleVar();

    ImGui::TextDisabled("Source Statistics");
    ImGui::Separator();

    if (font_mono_) ImGui::PushFont(font_mono_, font_mono_->LegacySize);
    std::lock_guard<std::mutex> lk(sources_mutex_);
    for (auto& src : sources_) {
        auto s = src->stats();
        double total  = static_cast<double>(s.packets_rx + s.packets_lost);
        double loss   = total > 0 ? 100.0 * s.packets_lost / total : 0.0;
        ImGui::Text("%-20s  RX %7llu  Lost %6llu  %.1f%%  %llu B",
                    src->label().c_str(),
                    (unsigned long long)s.packets_rx,
                    (unsigned long long)s.packets_lost,
                    loss,
                    (unsigned long long)s.bytes_rx);
    }
    if (font_mono_) ImGui::PopFont();

    ImGui::EndChild();
}

// ─────────────────────────────────────────────────────────────────────
// Add source modal
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_add_source_modal() {
    if (!ImGui::BeginPopupModal("AddSourceModal", nullptr,
            ImGuiWindowFlags_AlwaysAutoResize)) return;

    ImGui::TextUnformatted("Add UDP Source");
    ImGui::Separator();
    ImGui::Spacing();

    ImGui::InputText("Label", new_label_, sizeof(new_label_));
    ImGui::InputText("Host",  new_host_,  sizeof(new_host_));
    ImGui::InputInt ("Port",  &new_port_);
    new_port_ = std::clamp(new_port_, 1, 65535);

    ImGui::Spacing();
    ImGui::TextDisabled("Multicast (optional — leave blank for unicast)");
    ImGui::InputText("Multicast Group", new_mcast_group_, sizeof(new_mcast_group_));
    ImGui::InputInt ("Data Port",  &new_data_port_);
    if (new_data_port_ < 0) new_data_port_ = 0;
    if (new_data_port_ > 65535) new_data_port_ = 65535;
    if (new_data_port_ != 0)
        ImGui::TextDisabled("(0 = Port + 1)");

    ImGui::Spacing();
    ImGui::Separator();
    if (ImGui::Button("  Add  ", {80.f, 0.f})) {
        add_source(new_label_, new_host_,
                   static_cast<uint16_t>(new_port_),
                   new_mcast_group_,
                   static_cast<uint16_t>(new_data_port_));
        ImGui::CloseCurrentPopup();
    }
    ImGui::SameLine();
    if (ImGui::Button("Cancel", {80.f, 0.f}))
        ImGui::CloseCurrentPopup();

    ImGui::EndPopup();
}

// ─────────────────────────────────────────────────────────────────────
// Signal style popup
// ─────────────────────────────────────────────────────────────────────

void Renderer::draw_signal_style_popup() {
    ImGui::SetNextWindowSize({240.f, 0.f});
    if (!ImGui::BeginPopup("SignalStylePopup")) return;

    ImGui::TextUnformatted("Signal Style");
    ImGui::Separator();

    std::lock_guard<std::mutex> lk(sources_mutex_);
    if (style_src_idx_ >= 0 &&
        style_src_idx_ < static_cast<int>(sources_.size())) {
        auto& src = sources_[style_src_idx_];
        std::lock_guard<std::mutex> slk(src->signals_mutex);
        if (style_sig_idx_ >= 0 &&
            style_sig_idx_ < static_cast<int>(src->signal_states.size())) {
            auto& sig = src->signal_states[style_sig_idx_];
            ImGui::TextUnformatted(sig.def.name.c_str());
            ImGui::Separator();
            ImGui::ColorEdit4("Colour", sig.color,
                ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_PickerHueWheel);
            ImGui::SliderFloat("Width", &sig.line_width, 0.5f, 5.f, "%.1f px");
        }
    }
    if (ImGui::Button("Close", {-1.f, 0.f})) ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}

// ─────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────

double Renderer::compute_latest_time() const {
    double t = 0.0;
    std::lock_guard<std::mutex> lk(sources_mutex_);
    for (auto& src : sources_) {
        std::lock_guard<std::mutex> slk(src->signals_mutex);
        for (auto& sig : src->signal_states)
            if (sig.visible && sig.plot_mask != 0 && sig.buffer)
                t = std::max(t, sig.buffer->latest_time());
    }
    return t;
}
