/**
 * @file App.cpp
 * @brief Application state machine implementation.
 */

#include "App.h"
#include "SourcePanel.h"
#include "PlotPanel.h"
#include "TriggerPanel.h"
#include "StatsPanel.h"

#include "imgui.h"
#include "implot.h"
#include "Icons.h"

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

namespace StreamHubClient {

/* ── Layout icon helper ──────────────────────────────────────────────────── */

static void DrawLayoutIcon(ImDrawList* dl, ImVec2 tl, ImVec2 sz,
                           int cols, int rows, bool selected) {
    ImU32 bg   = selected ? IM_COL32(49,50,68,255) : IM_COL32(24,24,37,255);
    ImU32 cell = selected ? IM_COL32(137,180,250,200) : IM_COL32(88,91,112,160);
    ImU32 bord = selected ? IM_COL32(137,180,250,255) : IM_COL32(69,71,90,255);
    dl->AddRectFilled(tl, ImVec2(tl.x+sz.x, tl.y+sz.y), bg, 4.f);
    dl->AddRect(tl, ImVec2(tl.x+sz.x, tl.y+sz.y), bord, 4.f, 0, 1.f);
    float cw = sz.x / cols, rh = sz.y / rows;
    for (int c = 0; c < cols; c++) {
        for (int r = 0; r < rows; r++) {
            ImVec2 p0(tl.x + c*cw + 2.f, tl.y + r*rh + 2.f);
            ImVec2 p1(tl.x + (c+1)*cw - 2.f, tl.y + (r+1)*rh - 2.f);
            dl->AddRectFilled(p0, p1, cell, 2.f);
        }
    }
}

/* ── Signal ─────────────────────────────────────────────────────────────── */

/* ── App ─────────────────────────────────────────────────────────────────── */

App::App() {
    for (int i = 0; i < kMaxPlotSlots; i++) {
        plotPaused_[i]  = false;
        liveFollow_[i]  = true;
        activeSlot_[i]  = -1;
    }
}

App::~App() {
    shutdown();
}

void App::init(const std::string& host, uint16_t port) {
    snprintf(hostBuf_, sizeof(hostBuf_), "%s", host.c_str());
    snprintf(portBuf_, sizeof(portBuf_), "%u", static_cast<unsigned>(port));
    ws_.connect(host, port);
    /* Initial queries are sent on first connect transition in update() */
}

void App::shutdown() {
    ws_.disconnect();
}

/* ── Per-frame update ────────────────────────────────────────────────────── */

void App::update() {
    /* Send initial queries on first successful connect */
    bool nowConnected = ws_.isConnected();
    if (nowConnected && !prevConnected_) {
        ws_.sendText(BuildGetSources());
        ws_.sendText(BuildGetStats());
        ws_.sendText(BuildHistoryInfo());
    }
    prevConnected_ = nowConnected;

    /* Re-request history info periodically until signals are populated
     * (the hub opens history files after the first data packet, which is
     * after our initial connect-time query). */
    if (nowConnected && historyInfo_.enabled && historyInfo_.signals.empty()) {
        double now = ImGui::GetTime();
        if (now - lastHistInfoReq_ > 2.0) {
            ws_.sendText(BuildHistoryInfo());
            lastHistInfoReq_ = now;
        }
    }

    /* Drain WebSocket receive queue */
    ws_.poll([this](const WSMessage& msg) {
        if (msg.isBinary) {
            handleBinary(msg.data.data(), msg.data.size());
        } else {
            std::string json(reinterpret_cast<const char*>(msg.data.data()),
                             msg.data.size());
            handleJSON(json);
        }
    });

    /* ── Full-screen dockspace ─────────────────────────────────────────── */
    ImGuiViewport* vp = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(vp->WorkPos);
    ImGui::SetNextWindowSize(vp->WorkSize);

    ImGuiWindowFlags wf = ImGuiWindowFlags_NoTitleBar
                        | ImGuiWindowFlags_NoCollapse
                        | ImGuiWindowFlags_NoResize
                        | ImGuiWindowFlags_NoMove
                        | ImGuiWindowFlags_NoBringToFrontOnFocus
                        | ImGuiWindowFlags_NoNavFocus;

    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
    ImGui::Begin("##main", nullptr, wf);
    ImGui::PopStyleVar(3);

    renderToolbar();
    if (showHistBar_) { renderHistoryBar(); }
    if (showTrigBar_) { renderTriggerBar(); }

    /* Horizontal split: sidebar | plot grid */
    float sidebarW = sidebarOpen_ ? 220.f : 0.f;
    float avail    = ImGui::GetContentRegionAvail().x;
    float plotW    = avail - sidebarW;

    if (sidebarOpen_) {
        ImGui::BeginChild("##sidebar", ImVec2(sidebarW, 0.f), false,
                          ImGuiWindowFlags_NoMove);
        RenderSourcePanel(*this);
        ImGui::EndChild();
        ImGui::SameLine();
    }

    ImGui::BeginChild("##plots", ImVec2(plotW, 0.f), false);
    renderPlotGrid();
    ImGui::EndChild();

    ImGui::End();

    /* Overlays / modals */
    if (showStats_)   { renderStatsModal(); }
    if (showAddSrc_)  { renderAddSourceModal(); }
}

/* renderMenuBar() removed — all functionality merged into toolbar + sidebar */

/* ── Toolbar ─────────────────────────────────────────────────────────────── */

void App::renderToolbar() {
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));

    /* ── Sidebar toggle (leftmost) ───────────────────────────────────────── */
    {
        if (sidebarOpen_) {
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
        }
        if (ImGui::Button(ICON_FA_BARS "##sidebar")) { sidebarOpen_ = !sidebarOpen_; }
        if (sidebarOpen_) { ImGui::PopStyleColor(); }
        if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Toggle sidebar"); }
    }
    ImGui::SameLine();

    /* ── Layout picker — pictogram popup ─────────────────────────────────── */
    static const int kIconCols[] = {1,2,1,3,1,2,4,1};
    static const int kIconRows[] = {1,1,2,1,3,2,1,4};
    static const ImVec2 kIconSz  = ImVec2(36.f, 24.f);

    if (ImGui::Button(ICON_FA_TABLE_CELLS_LARGE "##layout")) {
        ImGui::OpenPopup("##layout_pop");
    }
    if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Layout"); }
    ImGui::SameLine();
    if (ImGui::BeginPopup("##layout_pop")) {
        ImGui::TextDisabled("Select layout");
        ImGui::Separator();
        ImDrawList* ldl = ImGui::GetWindowDrawList();
        for (int li = 0; li < static_cast<int>(PlotLayout::kCount); li++) {
            bool sel = (static_cast<int>(layout_) == li);
            ImVec2 p = ImGui::GetCursorScreenPos();
            if (ImGui::InvisibleButton(PlotLayoutNames[li], kIconSz)) {
                setLayout(static_cast<PlotLayout>(li));
                ImGui::CloseCurrentPopup();
            }
            DrawLayoutIcon(ldl, p, kIconSz,
                           kIconCols[li], kIconRows[li], sel);
            if (li < static_cast<int>(PlotLayout::kCount) - 1) {
                ImGui::SameLine(0.f, 4.f);
            }
        }
        ImGui::EndPopup();
    }

    /* ── Global pause ────────────────────────────────────────────────────── */
    if (ImGui::Button(globalPaused_ ? ICON_FA_PLAY " Resume"
                                    : ICON_FA_PAUSE " Pause")) {
        globalPaused_ = !globalPaused_;
        for (int i = 0; i < numPlotSlots(); i++) {
            plotPaused_[i] = globalPaused_;
            if (!globalPaused_) { liveFollow_[i] = true; }
        }
    }
    ImGui::SameLine();

    /* ── Cursors A/B toggle ──────────────────────────────────────────────── */
    {
        if (cursorsOn_) {
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
        }
        if (ImGui::Button(ICON_FA_CROSSHAIRS "##curs")) { cursorsOn_ = !cursorsOn_; }
        if (cursorsOn_) { ImGui::PopStyleColor(); }
        if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Cursors A/B"); }
    }
    ImGui::SameLine();

    /* ── Trigger bar toggle ──────────────────────────────────────────────── */
    {
        if (showTrigBar_) {
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
        }
        if (ImGui::Button(ICON_FA_BOLT "##trig")) { showTrigBar_ = !showTrigBar_; }
        if (showTrigBar_) { ImGui::PopStyleColor(); }
        if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Trigger"); }
    }
    ImGui::SameLine();

    /* ── History browse ──────────────────────────────────────────────────── */
    {
        bool histAvail = historyInfo_.enabled && !historyInfo_.signals.empty();
        if (!histAvail) { ImGui::BeginDisabled(); }
        if (showHistBar_) {
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
        }
        if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT "##hist")) {
            if (histAvail) {
                showHistBar_ = !showHistBar_;
                if (showHistBar_) {
                    /* On first open, jump to the full history range */
                    double ht0 = 1e300, ht1 = -1e300;
                    for (const auto& hs : historyInfo_.signals) {
                        if (hs.t0 < ht0) ht0 = hs.t0;
                        if (hs.t1 > ht1) ht1 = hs.t1;
                    }
                    if (ht1 > ht0) {
                        for (int i = 0; i < numPlotSlots(); i++) {
                            liveFollow_[i] = false;
                            setPlotX(i, ht0, ht1);
                            zoomCache_[i].valid = false;
                            zoomCache_[i].pending = false;
                            histZoomCache_[i].valid = false;
                            histZoomCache_[i].pending = false;
                            zoomHist_[i].clear();
                        }
                    }
                } else {
                    /* Closing the bar → back to live */
                    for (int i = 0; i < numPlotSlots(); i++) {
                        liveFollow_[i] = true;
                    }
                }
            }
        }
        if (showHistBar_) { ImGui::PopStyleColor(); }
        if (!histAvail) { ImGui::EndDisabled(); }
        if (ImGui::IsItemHovered()) { ImGui::SetTooltip("History browse"); }
    }
    ImGui::SameLine();

    /* ── Time window presets ─────────────────────────────────────────────── */
    static const double kWinPresets[] = {1.0, 5.0, 10.0, 30.0, 60.0};
    static const char*  kWinLabels[]  = {"1 s", "5 s", "10 s", "30 s", "60 s"};
    char winPreview[16];
    snprintf(winPreview, sizeof(winPreview), "%.3g s", windowSec_);
    ImGui::SetNextItemWidth(70.f);
    if (ImGui::BeginCombo("##win", winPreview)) {
        for (int wi = 0; wi < 5; wi++) {
            bool sel = std::fabs(windowSec_ - kWinPresets[wi]) < 1e-9;
            if (ImGui::Selectable(kWinLabels[wi], sel)) {
                windowSec_ = kWinPresets[wi];
            }
            if (sel) { ImGui::SetItemDefaultFocus(); }
        }
        ImGui::EndCombo();
    }

    /* ── Right-aligned group: [Stats] [Connection ▼] ● LED ──────────────── */
    bool connected = ws_.isConnected();
    const char* connLabel = connected
        ? ICON_FA_CIRCLE " Connected"
        : ICON_FA_CIRCLE " Disconnected";
    const ImGuiStyle& st = ImGui::GetStyle();

    const float kStatsW   = ImGui::CalcTextSize(ICON_FA_CHART_COLUMN).x
                            + st.FramePadding.x * 2.f;
    const float kConnBtnW = ImGui::CalcTextSize(ICON_FA_LINK).x
                            + st.FramePadding.x * 2.f;
    const float kLedW     = ImGui::CalcTextSize(connLabel).x + 8.f;
    const float kGroupW   = kStatsW + 4.f + kConnBtnW + 4.f + kLedW;

    float rightX = ImGui::GetCursorPosX() +
                   ImGui::GetContentRegionAvail().x - kGroupW;
    if (rightX > ImGui::GetCursorPosX()) {
        ImGui::SameLine(rightX);
    } else {
        ImGui::SameLine();
    }

    /* Stats toggle */
    {
        if (showStats_) {
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
        }
        if (ImGui::Button(ICON_FA_CHART_COLUMN "##stats")) { showStats_ = !showStats_; }
        if (showStats_) { ImGui::PopStyleColor(); }
        if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Statistics"); }
    }
    ImGui::SameLine(0.f, 4.f);

    /* Connection dropdown */
    if (ImGui::Button(ICON_FA_LINK "##conn")) {
        ImGui::OpenPopup("##conn_pop");
    }
    if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Connection"); }
    if (ImGui::BeginPopup("##conn_pop")) {
        ImGui::TextDisabled("Hub Connection");
        ImGui::Separator();
        ImGui::SetNextItemWidth(160.f);
        ImGui::InputText("Host##hubhost", hostBuf_, sizeof(hostBuf_));
        ImGui::SetNextItemWidth(80.f);
        ImGui::InputText("Port##hubport", portBuf_, sizeof(portBuf_),
                         ImGuiInputTextFlags_CharsDecimal);
        ImGui::Spacing();
        const char* btnLbl = connected ? "Reconnect" : "Connect";
        if (ImGui::Button(btnLbl, ImVec2(120.f, 0.f))) {
            ws_.reconnect(hostBuf_, static_cast<uint16_t>(atoi(portBuf_)));
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }
    ImGui::SameLine(0.f, 4.f);

    /* Status LED */
    ImVec4 ledColor = connected
        ? ImVec4(0.1f, 0.9f, 0.3f, 1.f)
        : ImVec4(0.9f, 0.2f, 0.2f, 1.f);
    ImGui::TextColored(ledColor, "%s", connLabel);

    ImGui::PopStyleVar();
    ImGui::Separator();
}

/* ── History browsing toolbar ────────────────────────────────────────────── */

void App::renderHistoryBar() {
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.980f,0.702f,0.529f,1.f));

    /* Title */
    ImGui::TextUnformatted(ICON_FA_CLOCK_ROTATE_LEFT " History");
    ImGui::PopStyleColor();
    ImGui::SameLine();

    /* ── Live button ── */
    if (ImGui::SmallButton(ICON_FA_TOWER_BROADCAST " Live")) {
        showHistBar_ = false;
        for (int i = 0; i < numPlotSlots(); i++) {
            liveFollow_[i] = true;
        }
        ImGui::PopStyleVar();
        return;
    }
    ImGui::SameLine();

    ImGui::TextDisabled("|"); ImGui::SameLine();

    /* ── Current time range display ── */
    double t0 = 0.0, t1 = 0.0;
    bool haveRange = false;
    for (int i = 0; i < numPlotSlots(); i++) {
        if (!liveFollow_[i]) {
            t0 = plotXMin_[i]; t1 = plotXMax_[i];
            haveRange = true;
            break;
        }
    }
    const double wallNow = std::chrono::duration<double>(
        std::chrono::system_clock::now().time_since_epoch()).count();

    if (haveRange) {
        double span = t1 - t0;
        double ago  = wallNow - t1;
        char rangeLabel[128];
        if (ago < 1.0) {
            snprintf(rangeLabel, sizeof(rangeLabel),
                     "%.3g s span | now", span);
        } else if (ago < 60.0) {
            snprintf(rangeLabel, sizeof(rangeLabel),
                     "%.3g s span | %.0f s ago", span, ago);
        } else if (ago < 3600.0) {
            snprintf(rangeLabel, sizeof(rangeLabel),
                     "%.3g s span | %.1f min ago", span, ago / 60.0);
        } else {
            snprintf(rangeLabel, sizeof(rangeLabel),
                     "%.3g s span | %.1f h ago", span, ago / 3600.0);
        }
        ImGui::TextDisabled("%s", rangeLabel);
    } else {
        ImGui::TextDisabled("No plot range");
    }
    ImGui::SameLine();

    /* ── Navigation: pan ← → ── */
    if (ImGui::SmallButton(ICON_FA_CHEVRON_LEFT "##hleft")) {
        for (int i = 0; i < numPlotSlots(); i++) {
            if (!liveFollow_[i]) {
                double span = plotXMax_[i] - plotXMin_[i];
                setPlotX(i, plotXMin_[i] - span * 0.75, plotXMax_[i] - span * 0.75);
            }
        }
    }
    if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pan left (75%% of window)"); }
    ImGui::SameLine(0.f, 2.f);

    if (ImGui::SmallButton(ICON_FA_CHEVRON_RIGHT "##hright")) {
        for (int i = 0; i < numPlotSlots(); i++) {
            if (!liveFollow_[i]) {
                double span = plotXMax_[i] - plotXMin_[i];
                double newMax = plotXMax_[i] + span * 0.75;
                /* Clamp: don't go past wallNow */
                if (newMax > wallNow) {
                    newMax = wallNow;
                    double newMin = newMax - span;
                    setPlotX(i, newMin, newMax);
                } else {
                    setPlotX(i, plotXMin_[i] + span * 0.75, newMax);
                }
            }
        }
    }
    if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pan right (75%% of window)"); }
    ImGui::SameLine();

    ImGui::TextDisabled("|"); ImGui::SameLine();

    /* ── Jump presets ── */
    static const double kJumpSec[]  = {  10.0,  30.0,  60.0,
                                         300.0, 600.0, 1800.0, 3600.0};
    static const char*  kJumpLabel[] = {"10 s", "30 s", "1 min",
                                         "5 min","10 min","30 min","1 h"};
    for (int j = 0; j < 7; j++) {
        char jid[32];
        snprintf(jid, sizeof(jid), "%s##hj%d", kJumpLabel[j], j);
        if (ImGui::SmallButton(jid)) {
            for (int i = 0; i < numPlotSlots(); i++) {
                liveFollow_[i] = false;
                double span = plotXMax_[i] - plotXMin_[i];
                if (span <= 0.0) { span = windowSec_; }
                setPlotX(i, wallNow - kJumpSec[j] - span,
                         wallNow - kJumpSec[j]);
            }
        }
        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip("Jump to %s ago", kJumpLabel[j]);
        }
        if (j < 6) { ImGui::SameLine(0.f, 2.f); }
    }
    ImGui::SameLine();

    /* ── All history ── */
    bool canAll = historyInfo_.enabled && !historyInfo_.signals.empty();
    if (!canAll) { ImGui::BeginDisabled(); }
    if (ImGui::SmallButton("All")) {
        double ht0 = 1e300, ht1 = -1e300;
        for (const auto& hs : historyInfo_.signals) {
            if (hs.t0 < ht0) ht0 = hs.t0;
            if (hs.t1 > ht1) ht1 = hs.t1;
        }
        if (ht1 > ht0) {
            for (int i = 0; i < numPlotSlots(); i++) {
                liveFollow_[i] = false;
                setPlotX(i, ht0, ht1);
                zoomCache_[i].valid = false;
                zoomCache_[i].pending = false;
                histZoomCache_[i].valid = false;
                histZoomCache_[i].pending = false;
                zoomHist_[i].clear();
            }
        }
    }
    if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Show all available history"); }
    if (!canAll) { ImGui::EndDisabled(); }

    ImGui::PopStyleVar();
    ImGui::Separator();
}

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

void App::renderPlotGrid() {
    int cols, rows;
    PlotLayoutDims(layout_, cols, rows);

    /* Per-cell fraction arrays (reset when layout changes) */
    static float      rowFrac[8]    = {1,1,1,1,1,1,1,1};
    static float      colFrac[8]    = {1,1,1,1,1,1,1,1};
    static PlotLayout prevLayout    = PlotLayout::kCount;
    if (layout_ != prevLayout) {
        for (int i = 0; i < 8; i++) { rowFrac[i] = 1.f; colFrac[i] = 1.f; }
        prevLayout = layout_;
    }

    const float kSep = 5.f;   /* separator strip width/height in px */

    float avW = ImGui::GetContentRegionAvail().x;
    float avH = ImGui::GetContentRegionAvail().y;
    float cellTotalW = avW - kSep * (cols - 1);
    float cellTotalH = avH - kSep * (rows - 1);

    /* Normalise fractions */
    float rowSum = 0.f; for (int r = 0; r < rows; r++) rowSum += rowFrac[r];
    float colSum = 0.f; for (int c = 0; c < cols; c++) colSum += colFrac[c];
    if (rowSum < 1e-6f) rowSum = 1.f;
    if (colSum < 1e-6f) colSum = 1.f;

    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, 0.f));

    for (int r = 0; r < rows; r++) {
        float rh = std::max(30.f, cellTotalH * rowFrac[r] / rowSum);

        for (int c = 0; c < cols; c++) {
            float cw = std::max(60.f, cellTotalW * colFrac[c] / colSum);
            int plotIdx = r * cols + c;

            char cellId[32]; snprintf(cellId, sizeof(cellId), "##cell%d", plotIdx);
            ImGui::BeginChild(cellId, ImVec2(cw, rh), false,
                ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
            if (plotIdx < kMaxPlotSlots) {
                RenderPlotPanel(*this, plotIdx, plotPaused_[plotIdx]);
            }
            ImGui::EndChild();

            /* ── Column separator ── */
            if (c < cols - 1) {
                ImGui::SameLine(0.f, 0.f);
                char csId[32]; snprintf(csId, sizeof(csId), "##cs%d_%d", r, c);
                ImVec2 p0 = ImGui::GetCursorScreenPos();
                ImGui::InvisibleButton(csId, ImVec2(kSep, rh));
                if (ImGui::IsItemActive()) {
                    float dx = ImGui::GetIO().MouseDelta.x;
                    colFrac[c]   += dx * colSum / cellTotalW;
                    colFrac[c+1] -= dx * colSum / cellTotalW;
                    colFrac[c]   = std::max(0.05f * colSum, colFrac[c]);
                    colFrac[c+1] = std::max(0.05f * colSum, colFrac[c+1]);
                }
                if (ImGui::IsItemHovered() || ImGui::IsItemActive())
                    ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
                ImGui::GetWindowDrawList()->AddRectFilled(
                    p0, ImVec2(p0.x + kSep, p0.y + rh), IM_COL32(49,50,68,255));
                ImGui::SameLine(0.f, 0.f);
            }
        } /* end col loop */

        /* ── Row separator ── */
        if (r < rows - 1) {
            char rsId[32]; snprintf(rsId, sizeof(rsId), "##rs%d", r);
            /* Reposition X to left content edge before drawing full-width strip */
            ImGui::SetCursorPosX(0.f);
            ImVec2 p0 = ImGui::GetCursorScreenPos();
            ImGui::InvisibleButton(rsId, ImVec2(avW, kSep));
            if (ImGui::IsItemActive()) {
                float dy = ImGui::GetIO().MouseDelta.y;
                rowFrac[r]   += dy * rowSum / cellTotalH;
                rowFrac[r+1] -= dy * rowSum / cellTotalH;
                rowFrac[r]   = std::max(0.05f * rowSum, rowFrac[r]);
                rowFrac[r+1] = std::max(0.05f * rowSum, rowFrac[r+1]);
            }
            if (ImGui::IsItemHovered() || ImGui::IsItemActive())
                ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
            ImGui::GetWindowDrawList()->AddRectFilled(
                p0, ImVec2(p0.x + avW, p0.y + kSep), IM_COL32(49,50,68,255));
        }
    } /* end row loop */

    ImGui::PopStyleVar();
}

/* ── Stats modal ─────────────────────────────────────────────────────────── */

void App::renderStatsModal() {
    ImGui::SetNextWindowSize(ImVec2(700.f, 300.f), ImGuiCond_FirstUseEver);
    if (ImGui::Begin("Statistics", &showStats_)) {
        RenderStatsPanel(*this);
    }
    ImGui::End();
}

/* ── Add-source modal ────────────────────────────────────────────────────── */

void App::renderAddSourceModal() {
    ImGui::SetNextWindowSize(ImVec2(360.f, 260.f), ImGuiCond_Always);
    ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
                            ImGuiCond_Always, ImVec2(0.5f, 0.5f));
    if (ImGui::Begin("Add Source", &showAddSrc_,
                     ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) {
        static char label[128]= "";
        static char host[64]  = "127.0.0.1";
        static int  port      = 44500;
        static char mcast[64] = "";
        static int  dataPort  = 0;

        ImGui::InputText("Label", label, sizeof(label));
        ImGui::InputText("Host",  host,  sizeof(host));
        ImGui::InputInt("Port",  &port);
        ImGui::InputText("Multicast", mcast, sizeof(mcast));
        ImGui::InputInt("Data Port", &dataPort);

        ImGui::Spacing();
        if (ImGui::Button("Connect", ImVec2(100.f, 0.f))) {
            if (host[0] != '\0' && port > 0 && port < 65536) {
                char addr[96];
                snprintf(addr, sizeof(addr), "%s:%d", host, port);
                ws_.sendText(BuildAddSource(label, addr, mcast,
                                            static_cast<uint16_t>(
                                                (dataPort > 0 && dataPort < 65536)
                                                    ? dataPort : 0)));
                showAddSrc_ = false;
                label[0] = '\0';
            }
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel", ImVec2(80.f, 0.f))) {
            showAddSrc_ = false;
        }
    }
    ImGui::End();
}

/* ── Trigger bar ─────────────────────────────────────────────────────────── */

void App::renderTriggerBar() {
    RenderTriggerPanel(*this);
}

/* ── Layout ──────────────────────────────────────────────────────────────── */

int App::numPlotSlots() const {
    int cols, rows;
    PlotLayoutDims(layout_, cols, rows);
    return cols * rows;
}

void App::setLayout(PlotLayout l) {
    layout_ = l;
}

/* ── Source helpers ──────────────────────────────────────────────────────── */

int App::findSource(const std::string& id) const {
    for (int i = 0; i < static_cast<int>(sources_.size()); i++) {
        if (sources_[i].id == id) { return i; }
    }
    return -1;
}

int App::findSignal(const Source& src, const std::string& name) const {
    for (int i = 0; i < static_cast<int>(src.signals.size()); i++) {
        if (src.signals[i].meta.name == name) { return i; }
    }
    return -1;
}

/* ── WS message dispatch ─────────────────────────────────────────────────── */

void App::handleJSON(const std::string& json) {
    std::string type = ParseType(json);

    if      (type == "sources")          { onSources(json); }
    else if (type == "config")           { onConfig(json); }
    else if (type == "stats")            { onStats(json); }
    else if (type == "triggerState")     { onTriggerState(json); }
    else if (type == "zoom")             { onZoom(json); }
    else if (type == "historyZoom")      { onHistoryZoom(json); }
    else if (type == "historyInfo")      { onHistoryInfo(json); }
    else if (type == "maxPointsUpdated") { onMaxPointsUpdated(json); }
    /* pong: ignore */
}

void App::handleBinary(const uint8_t* data, size_t len) {
    if (len == 0) { return; }

    /* Version 2: trigger capture frame */
    if (data[0] == 2u) {
        CaptureFrame cf;
        if (ParseCaptureFrame(data, len, cf)) {
            capture_    = std::move(cf);
            hasCapture_ = true;
            trigger_.status      = "triggered";
            trigger_.trigTime    = capture_.trigTime;
            trigger_.hasTrigTime = true;
            resetTrigZoom();
        }
        return;
    }

    /* Version 1: live push frame. The hub pushes only samples that are new
     * since the previous tick (non-overlapping), so push verbatim. */
    DataFrame frame;
    if (!ParseBinaryFrame(data, len, frame)) { return; }

    std::lock_guard<std::mutex> lk(srcMutex_);
    int srcIdx = findSource(frame.sourceId);
    if (srcIdx < 0) { return; }

    Source& src = sources_[srcIdx];
    for (const auto& fs : frame.signals) {
        int sigIdx = findSignal(src, fs.name);
        if (sigIdx < 0) { continue; }
        /* Always push: the ring is the "active" buffer; paused plots render
         * from their frozen view snapshot, so no data is ever dropped. */
        size_t n = std::min(fs.t.size(), fs.v.size());
        auto& buf = src.signals[sigIdx].buf;
        for (size_t i = 0; i < n; i++) {
            buf.push(fs.t[i], fs.v[i]);
        }
    }
}

/* ── JSON event handlers ─────────────────────────────────────────────────── */

void App::onSources(const std::string& json) {
    std::vector<SourceInfo> infos;
    ParseSources(json, infos);

    std::lock_guard<std::mutex> lk(srcMutex_);
    for (const auto& info : infos) {
        int idx = findSource(info.id);
        if (idx < 0) {
            Source s;
            s.id    = info.id;
            s.label = info.label;
            s.addr  = info.addr;
            s.port  = info.port;
            s.state = info.state;
            sources_.push_back(std::move(s));
        } else {
            sources_[idx].state = info.state;
            sources_[idx].label = info.label;
        }
    }

    /* Remove sources that are no longer listed */
    sources_.erase(std::remove_if(sources_.begin(), sources_.end(),
        [&infos](const Source& s) {
            for (const auto& i : infos) { if (i.id == s.id) { return false; } }
            return true;
        }), sources_.end());
}

void App::onConfig(const std::string& json) {
    std::string sourceId;
    int         publishMode = 0;
    std::vector<SignalMeta> metas;

    if (!ParseConfig(json, sourceId, publishMode, metas)) { return; }

    std::lock_guard<std::mutex> lk(srcMutex_);
    int idx = findSource(sourceId);
    if (idx < 0) { return; }

    Source& src = sources_[idx];
    src.configured  = true;
    src.publishMode = publishMode;

    /* Colour palette — Catppuccin Mocha trace colours */
    static const ImVec4 kPalette[] = {
        {0.537f,0.706f,0.980f,1.f}, /* blue */
        {0.651f,0.890f,0.631f,1.f}, /* green */
        {0.953f,0.545f,0.659f,1.f}, /* red */
        {0.980f,0.702f,0.529f,1.f}, /* peach */
        {0.796f,0.651f,0.969f,1.f}, /* mauve */
        {0.580f,0.886f,0.835f,1.f}, /* teal */
        {0.537f,0.863f,0.922f,1.f}, /* sky */
        {0.706f,0.745f,0.996f,1.f}, /* lavender */
    };

    /* Merge: add new signals, keep existing ring buffers */
    for (size_t m = 0; m < metas.size(); m++) {
        int si = findSignal(src, metas[m].name);
        if (si < 0) {
            Signal sig;
            sig.meta = metas[m];
            sig.color = kPalette[src.signals.size() % 8];
            src.signals.push_back(std::move(sig));
        } else {
            src.signals[si].meta = metas[m];
        }
    }
}

void App::onStats(const std::string& json) {
    std::vector<std::pair<std::string, SourceStats>> statsVec;
    ParseStats(json, statsVec);

    std::lock_guard<std::mutex> lk(srcMutex_);
    for (const auto& kv : statsVec) {
        int idx = findSource(kv.first);
        if (idx >= 0) {
            sources_[idx].stats = kv.second;
            sources_[idx].state = kv.second.state;
        }
    }
}

void App::onTriggerState(const std::string& json) {
    TriggerStateMsg msg;
    if (!ParseTriggerState(json, msg)) { return; }

    trigger_.status  = msg.state;
    trigger_.stopped = msg.stopped;
    if (msg.hasTrigTime) {
        trigger_.trigTime    = msg.trigTime;
        trigger_.hasTrigTime = true;
    }
    /* Double-buffer semantics: the last recorded capture stays on display
     * (even while re-armed/collecting) and is only replaced when a new
     * capture frame has been fully received and parsed (handleBinary v2). */
    if (msg.state == "idle") {
        trigger_.hasTrigTime = false;
    }
}

void App::onZoom(const std::string& json) {
    ZoomResponse resp;
    if (!ParseZoom(json, resp)) { return; }

    /* Route the reply to the plot that requested it */
    for (int i = 0; i < kMaxPlotSlots; i++) {
        auto& zc = zoomCache_[i];
        if (zc.pending && zc.reqId == resp.reqId) {
            /* Compute the actual time range from the returned data so the
             * coverage check in PlotPanel can distinguish between "ring had
             * data for the full window" and "ring only had partial data". */
            double dataT0 = 1e300, dataT1 = -1e300;
            bool anyData = false;
            for (const auto& zs : resp.signals) {
                for (double tv : zs.t) {
                    if (tv < dataT0) { dataT0 = tv; }
                    if (tv > dataT1) { dataT1 = tv; }
                    anyData = true;
                }
            }
            if (anyData) {
                zc.signals = std::move(resp.signals);
                zc.t0      = dataT0;
                zc.t1      = dataT1;
                zc.valid   = true;
            }
            zc.pending = false;
            return;
        }
    }
}

void App::requestZoom(int plotIdx, double t0, double t1,
                      const std::string& signalsCsv) {
    if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; }
    if (!ws_.isConnected() || signalsCsv.empty()) { return; }
    auto& zc = zoomCache_[plotIdx];
    zc.reqId   = nextZoomReqId_++;
    zc.reqT0   = t0;
    zc.reqT1   = t1;
    zc.pending = true;
    ws_.sendText(BuildZoom(zc.reqId, t0, t1, 2400, signalsCsv));
}

std::string App::slotKey(const PlotAssignment& a) const {
    if (a.sourceIdx < 0 || a.sourceIdx >= static_cast<int>(sources_.size())) {
        return std::string();
    }
    const Source& src = sources_[a.sourceIdx];
    if (a.signalIdx < 0 || a.signalIdx >= static_cast<int>(src.signals.size())) {
        return std::string();
    }
    return src.id + ":" + src.signals[a.signalIdx].meta.name;
}

void App::onMaxPointsUpdated(const std::string& json) {
    uint32_t mp = 0;
    ParseMaxPointsUpdated(json, mp);
    if (mp >= 2) {
        maxPoints_ = mp;
        std::lock_guard<std::mutex> lk(srcMutex_);
        for (auto& src : sources_) {
            for (auto& sig : src.signals) {
                sig.buf.setCapacity(mp);
            }
        }
    }
}

/* ---- History ------------------------------------------------------------ */

void App::onHistoryZoom(const std::string& json) {
    ZoomResponse resp;
    if (!ParseZoom(json, resp)) { return; }

    /* Route the reply to the plot that requested it (via histZoomCache_) */
    for (int i = 0; i < kMaxPlotSlots; i++) {
        auto& hc = histZoomCache_[i];
        if (hc.pending && hc.reqId == resp.reqId) {
            /* Compute the actual time range from returned data. */
            double dataT0 = 1e300, dataT1 = -1e300;
            bool anyData = false;
            for (const auto& zs : resp.signals) {
                for (double tv : zs.t) {
                    if (tv < dataT0) { dataT0 = tv; }
                    if (tv > dataT1) { dataT1 = tv; }
                    anyData = true;
                }
            }
            if (anyData) {
                hc.signals = std::move(resp.signals);
                hc.t0      = dataT0;
                hc.t1      = dataT1;
                hc.valid   = true;
            }
            hc.pending = false;
            return;
        }
    }
}

void App::onHistoryInfo(const std::string& json) {
    ParseHistoryInfo(json, historyInfo_);
}

void App::requestHistoryZoom(int plotIdx, double t0, double t1,
                              const std::string& signalsCsv) {
    if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; }
    if (!ws_.isConnected() || signalsCsv.empty()) { return; }
    auto& hc = histZoomCache_[plotIdx];
    hc.reqId   = nextZoomReqId_++;
    hc.reqT0   = t0;
    hc.reqT1   = t1;
    hc.pending = true;
    ws_.sendText(BuildHistoryZoom(hc.reqId, t0, t1, 2400, signalsCsv));
}

} /* namespace StreamHubClient */
