/**
 * @file PlotWidget.cpp
 */

#include "PlotWidget.h"
#include "Hub.h"
#include "Theme.h"

#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QToolButton>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QMenu>
#include <QColorDialog>
#include <QInputDialog>
#include <QDateTime>
#include <QPolygonF>

#include <algorithm>
#include <cmath>
#include <cstdio>

using namespace StreamHubClient;

namespace shq {

static const char* kMimeSignal = "application/x-shq-signal";
static const size_t kMaxPush    = 2400;
static const double kLiveHiResMaxWin = 600.0;

static double nowSec() {
    return QDateTime::currentMSecsSinceEpoch() / 1000.0;
}

/* ── ported math helpers ─────────────────────────────────────────────────── */

static QString fmtVal(double v) {
    if (!std::isfinite(v)) { return QStringLiteral("?"); }
    char buf[32];
    double a = std::fabs(v);
    if (v == 0.0)                  std::snprintf(buf, sizeof(buf), "0");
    else if (a >= 1e4 || a < 1e-3) std::snprintf(buf, sizeof(buf), "%.2e", v);
    else                           std::snprintf(buf, sizeof(buf), "%.3g", v);
    return QString::fromLatin1(buf);
}

static void resolveVScale(PlotAssignment& a, const SignalView& sig,
                          const std::vector<double>& rawV) {
    if (a.vs.mode == 1) {
        double rmin = sig.meta.rangeMin, rmax = sig.meta.rangeMax;
        if (rmax > rmin) {
            a.vs.resolvedDiv    = std::max((rmax - rmin) / 8.0, 1e-30);
            a.vs.resolvedOffset = (rmin + rmax) / 2.0;
            return;
        }
    }
    if (a.vs.mode == 2) {
        a.vs.resolvedDiv    = std::max(a.vs.divValue, 1e-30);
        a.vs.resolvedOffset = a.vs.offset;
        return;
    }
    double mn = 1e300, mx = -1e300;
    for (double v : rawV) { if (v < mn) mn = v; if (v > mx) mx = v; }
    if (!std::isfinite(mn) || mn > mx) { mn = -1.0; mx = 1.0; }
    if (mn == mx) { mn -= 1.0; mx += 1.0; }
    a.vs.resolvedDiv    = std::max((mx - mn) / 6.0, 1e-30);
    a.vs.resolvedOffset = (mx + mn) / 2.0;
}

static double normalizeY(double raw, const VScale& vs) {
    return (raw - vs.resolvedOffset) / vs.resolvedDiv + vs.screenPos;
}

static bool dataMinMax(const std::vector<double>& v, double& mn, double& mx) {
    mn = 1e300; mx = -1e300;
    for (double x : v) { if (std::isfinite(x)) { if (x < mn) mn = x; if (x > mx) mx = x; } }
    return mx >= mn;
}

static void bandNormalize(const std::vector<double>& raw, std::vector<double>& out,
                          int ki, int n, bool quantize) {
    double bandH   = 8.0 / std::max(n, 1);
    double centerY = 4.0 - (ki + 0.5) * bandH;
    double hi = centerY + bandH * 0.35;
    double lo = centerY - bandH * 0.35;
    double mn, mx;
    bool haveMM = dataMinMax(raw, mn, mx);
    out.resize(raw.size());
    if (quantize) {
        double thr = haveMM ? (mn + mx) / 2.0 : 0.5;
        for (size_t i = 0; i < raw.size(); i++) {
            double v = raw[i];
            out[i] = !std::isfinite(v) ? NAN : (v >= thr ? hi : lo);
        }
    } else {
        if (!haveMM) { mn = 0.0; mx = 1.0; }
        if (mn == mx) { mn -= 1.0; mx += 1.0; }
        double range = mx - mn, bandRange = hi - lo;
        for (size_t i = 0; i < raw.size(); i++) {
            double v = raw[i];
            out[i] = !std::isfinite(v) ? NAN : lo + (v - mn) / range * bandRange;
        }
    }
}

static double sampleAt(const std::vector<double>& t, const std::vector<double>& v,
                       double x) {
    if (t.empty() || v.size() != t.size()) { return NAN; }
    auto it = std::lower_bound(t.begin(), t.end(), x);
    size_t i = static_cast<size_t>(it - t.begin());
    if (i >= t.size()) { i = t.size() - 1; }
    else if (i > 0 && (x - t[i-1]) < (t[i] - x)) { i--; }
    return v[i];
}

/*===========================================================================*/
/* PlotCanvas                                                                 */
/*===========================================================================*/

class PlotCanvas : public QWidget {
public:
    explicit PlotCanvas(PlotWidget* w) : QWidget(w), w_(w) {
        setMinimumSize(80, 60);
        setAcceptDrops(true);
        setMouseTracking(true);
    }

protected:
    void paintEvent(QPaintEvent*) override;
    void wheelEvent(QWheelEvent* e) override;
    void mousePressEvent(QMouseEvent* e) override;
    void mouseMoveEvent(QMouseEvent* e) override;
    void mouseReleaseEvent(QMouseEvent* e) override;
    void dragEnterEvent(QDragEnterEvent* e) override;
    void dropEvent(QDropEvent* e) override;

private:
    /* geometry of the plot drawing rect */
    QRectF plotRect() const {
        const double L = 56, R = 12, T = 8, B = 24;
        return QRectF(L, T, std::max(10.0, width() - L - R),
                      std::max(10.0, height() - T - B));
    }
    double xToPx(double t, double xMin, double xMax, const QRectF& r) const {
        if (xMax <= xMin) return r.left();
        return r.left() + (t - xMin) / (xMax - xMin) * r.width();
    }
    double pxToX(double px, double xMin, double xMax, const QRectF& r) const {
        if (r.width() <= 0) return xMin;
        return xMin + (px - r.left()) / r.width() * (xMax - xMin);
    }
    double yToPx(double yDiv, const QRectF& r) const {
        return r.top() + (4.05 - yDiv) / 8.10 * r.height();
    }
    void drawMarker(QPainter& p, double cx, double cy, int marker, double sz);

    PlotWidget* w_;

    /* interaction state */
    bool   panning_     = false;
    int    dragCursor_  = 0;   /* 0 none, 1 A, 2 B */
    QPoint lastPos_;
    double curXMin_ = 0, curXMax_ = 1;  /* last painted X range (for hit-test) */
};

void PlotCanvas::drawMarker(QPainter& p, double cx, double cy, int marker, double sz) {
    double h = sz;
    switch (marker) {
        case 0: p.drawEllipse(QPointF(cx, cy), h, h); break;            /* circle */
        case 1: p.drawRect(QRectF(cx-h, cy-h, 2*h, 2*h)); break;        /* square */
        case 2: { QPolygonF d; d << QPointF(cx,cy-h)<<QPointF(cx+h,cy)
                      <<QPointF(cx,cy+h)<<QPointF(cx-h,cy); p.drawPolygon(d); } break;
        case 3: { QPolygonF u; u<<QPointF(cx,cy-h)<<QPointF(cx+h,cy+h)
                      <<QPointF(cx-h,cy+h); p.drawPolygon(u); } break;  /* up */
        case 4: { QPolygonF d; d<<QPointF(cx,cy+h)<<QPointF(cx+h,cy-h)
                      <<QPointF(cx-h,cy-h); p.drawPolygon(d); } break;  /* down */
        case 7: p.drawLine(QPointF(cx-h,cy-h),QPointF(cx+h,cy+h));
                p.drawLine(QPointF(cx-h,cy+h),QPointF(cx+h,cy-h)); break;/* cross */
        case 8: p.drawLine(QPointF(cx-h,cy),QPointF(cx+h,cy));
                p.drawLine(QPointF(cx,cy-h),QPointF(cx,cy+h)); break;    /* plus */
        default: p.drawEllipse(QPointF(cx, cy), h, h); break;
    }
}

void PlotCanvas::paintEvent(QPaintEvent*) {
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing, true);

    Hub* hub = w_->hub_;
    GlobalView* gv = w_->gv_;
    auto& slots   = w_->slots_;
    auto& sources = hub->sources();

    const QRectF r = plotRect();
    const double wallNow = nowSec();

    /* background + border */
    p.fillRect(rect(), col::base());
    p.fillRect(r, col::crust());

    const CaptureFrame* cap = hub->capture();
    const bool trigView = (cap != nullptr) && gv->trigView;
    auto& zc = hub->zoomCache(w_->plotIdx_);
    auto& hc = hub->histZoomCache(w_->plotIdx_);
    const bool paused = w_->paused_;
    bool& live = w_->live_;

    /* ── pause snapshot ─────────────────────────────────────────────────── */
    auto& snap = w_->snap_;
    if (paused) {
        if (!snap.valid) {
            snap.keys.clear(); snap.t.clear(); snap.v.clear();
            for (const auto& a : slots) {
                std::vector<double> tt, vv;
                if (a.sourceIdx >= 0 && a.sourceIdx < (int)sources.size() &&
                    a.signalIdx >= 0 &&
                    a.signalIdx < (int)sources[a.sourceIdx].signals.size()) {
                    sources[a.sourceIdx].signals[a.signalIdx]
                        .buf.readLast((size_t)hub->maxPoints(), tt, vv);
                }
                snap.keys.push_back(hub->slotKey(a));
                snap.t.push_back(std::move(tt));
                snap.v.push_back(std::move(vv));
            }
            snap.valid = true;
            if (live) { w_->initPlotX(wallNow); live = false; }
        }
    } else if (snap.valid) {
        snap.valid = false;
    }

    /* ── gather data per slot ───────────────────────────────────────────── */
    std::vector<std::vector<double>> tStore(slots.size()), vStore(slots.size());

    const bool liveHiRes = !trigView && live && !paused &&
        gv->windowSec <= kLiveHiResMaxWin && zc.valid &&
        (zc.t1 - zc.t0) >= gv->windowSec * 0.9 && (wallNow - zc.t1) < 3.0;

    const bool useZoomData = !trigView && !paused && zc.valid &&
        (liveHiRes ||
         (!live && zc.t0 <= w_->plotXMin_ + 1e-9 && zc.t1 >= w_->plotXMax_ - 1e-9));

    bool useHistData = !trigView && !paused && !live && hc.valid &&
        hc.t0 <= w_->plotXMin_ + 1e-9 && hc.t1 >= w_->plotXMax_ - 1e-9;
    if (useHistData) {
        bool any = false;
        for (const auto& hs : hc.pts) { if (!hs.t.empty()) { any = true; break; } }
        if (!any) { useHistData = false; }
    }

    auto readBase = [&](size_t si, const SignalView& sig, const std::string& key) {
        if (paused && snap.valid) {
            for (size_t k = 0; k < snap.keys.size(); k++) {
                if (snap.keys[k] == key) { tStore[si] = snap.t[k]; vStore[si] = snap.v[k]; return; }
            }
        }
        sig.buf.readLast((size_t)hub->maxPoints(), tStore[si], vStore[si]);
    };

    for (size_t si = 0; si < slots.size(); si++) {
        auto& a = slots[si];
        if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
        if (a.signalIdx < 0 || a.signalIdx >= (int)sources[a.sourceIdx].signals.size()) continue;
        const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
        const std::string key = hub->slotKey(a);

        if (trigView) {
            for (const auto& cs : cap->signals) {
                if (cs.key != key) continue;
                size_t n = std::min(cs.t.size(), cs.v.size());
                tStore[si].reserve(n); vStore[si].reserve(n);
                for (size_t i = 0; i < n; i++) {
                    tStore[si].push_back(cs.t[i] - cap->trigTime);
                    vStore[si].push_back(cs.v[i]);
                }
                break;
            }
        } else if (useZoomData) {
            bool found = false;
            for (const auto& zs : zc.pts) {
                if (zs.name != key) continue;
                tStore[si] = zs.t; vStore[si] = zs.v; found = true; break;
            }
            if (!found) readBase(si, sig, key);
        } else if (useHistData) {
            bool found = false;
            for (const auto& hs : hc.pts) {
                if (hs.name != key) continue;
                tStore[si] = hs.t; vStore[si] = hs.v; found = true; break;
            }
            if (!found) readBase(si, sig, key);
        } else {
            readBase(si, sig, key);
        }
        resolveVScale(a, sig, vStore[si]);
    }

    /* ── X range ────────────────────────────────────────────────────────── */
    double xMin, xMax;
    if (trigView) {
        if (w_->trigZoomed_) { xMin = w_->plotXMin_; xMax = w_->plotXMax_; }
        else { xMin = -cap->preSec; xMax = cap->postSec; }
    } else if (live && !paused) {
        if (liveHiRes) { xMax = zc.t1; xMin = zc.t1 - gv->windowSec; }
        else           { xMax = wallNow; xMin = wallNow - gv->windowSec; }
    } else {
        xMin = w_->plotXMin_; xMax = w_->plotXMax_;
    }
    if (xMax <= xMin) { xMax = xMin + 1.0; }
    curXMin_ = xMin; curXMax_ = xMax;

    /* ── grid + ticks ───────────────────────────────────────────────────── */
    p.setPen(QPen(QColor(0x31,0x32,0x44,160), 1.0));
    /* Y grid: 9 division lines */
    const auto& av = (w_->vMode_ == 0 && w_->activeSlot_ >= 0 &&
                      w_->activeSlot_ < (int)slots.size())
                     ? slots[w_->activeSlot_].vs : VScale();
    p.setFont(QFont(font().family(), 8));
    for (int d = -4; d <= 4; d++) {
        double y = yToPx(d, r);
        p.setPen(QPen(QColor(0x31,0x32,0x44, d==0?220:120), d==0?1.2:1.0));
        p.drawLine(QPointF(r.left(), y), QPointF(r.right(), y));
        QString lbl;
        if (w_->vMode_ == 0 && w_->activeSlot_ >= 0 &&
            w_->activeSlot_ < (int)slots.size()) {
            double rawVal = av.resolvedOffset + (d - av.screenPos) * av.resolvedDiv;
            lbl = fmtVal(rawVal);
        } else {
            lbl = QString::number(d);
        }
        p.setPen(QColor(0xa6,0xad,0xc8));
        p.drawText(QRectF(0, y-8, r.left()-4, 16),
                   Qt::AlignRight|Qt::AlignVCenter, lbl);
    }
    /* X grid: 11 ticks */
    for (int t = 0; t <= 10; t++) {
        double xv = xMin + (xMax - xMin) * t / 10.0;
        double x  = xToPx(xv, xMin, xMax, r);
        p.setPen(QPen(QColor(0x31,0x32,0x44,120), 1.0));
        p.drawLine(QPointF(x, r.top()), QPointF(x, r.bottom()));
        p.setPen(QColor(0xa6,0xad,0xc8));
        QString xl = trigView ? fmtVal(xv) + "s" : QString::number(xv, 'f', 3);
        int flags = (t==0?Qt::AlignLeft:(t==10?Qt::AlignRight:Qt::AlignHCenter))
                    | Qt::AlignTop;
        p.drawText(QRectF(x-40, r.bottom()+2, 80, 14), flags, xl);
    }
    p.setPen(QPen(col::surface0(), 1.0));
    p.drawRect(r);

    /* clip drawing to the plot rect */
    p.save();
    p.setClipRect(r);

    /* ── clip data to visible window, LTTB, normalize, draw ─────────────── */
    int nTraces = 0;
    for (const auto& a : slots) {
        if (a.sourceIdx >= 0 && a.sourceIdx < (int)sources.size()) nTraces++;
    }

    std::vector<double> tDec, vDec, vNorm;
    int ki = 0;
    for (size_t si = 0; si < slots.size(); si++) {
        auto& a = slots[si];
        if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
        if (a.signalIdx < 0 || a.signalIdx >= (int)sources[a.sourceIdx].signals.size()) continue;
        const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
        int myKi = ki++;
        if (!sig.visible) continue;
        auto tv = tStore[si];
        auto vv = vStore[si];
        if (tv.empty()) continue;

        /* clip to [xMin,xMax] keeping one sample each side */
        auto lo = std::lower_bound(tv.begin(), tv.end(), xMin);
        auto hi = std::upper_bound(lo, tv.end(), xMax);
        if (lo != tv.begin()) --lo;
        if (hi != tv.end())   ++hi;
        size_t i0 = (size_t)(lo - tv.begin());
        size_t i1 = (size_t)(hi - tv.begin());
        if (i0 > 0 || i1 < tv.size()) {
            tv = std::vector<double>(tv.begin()+i0, tv.begin()+i1);
            vv = std::vector<double>(vv.begin()+i0, vv.begin()+i1);
        }
        if (tv.empty()) continue;

        size_t nOut = LTTBDecimate(tv, vv, tDec, vDec, kMaxPush);
        if (nOut == 0) continue;

        if (w_->vMode_ == 1)      bandNormalize(vDec, vNorm, myKi, nTraces, true);
        else if (w_->vMode_ == 2) bandNormalize(vDec, vNorm, myKi, nTraces, a.vs.digitalInMixed);
        else {
            vNorm.resize(nOut);
            for (size_t k = 0; k < nOut; k++) vNorm[k] = normalizeY(vDec[k], a.vs);
        }

        QColor c = sig.color;
        p.setPen(QPen(c, sig.lineWidth));
        QPolygonF poly;
        poly.reserve((int)nOut);
        for (size_t k = 0; k < nOut; k++) {
            if (!std::isfinite(vNorm[k])) {
                if (poly.size() > 1) p.drawPolyline(poly);
                poly.clear();
                continue;
            }
            poly << QPointF(xToPx(tDec[k], xMin, xMax, r), yToPx(vNorm[k], r));
        }
        if (poly.size() > 1) p.drawPolyline(poly);
        else if (poly.size() == 1) p.drawPoint(poly.front());

        if (sig.marker >= 0) {
            p.setBrush(c);
            for (const QPointF& pt : poly) drawMarker(p, pt.x(), pt.y(), sig.marker, 2.5);
            p.setBrush(Qt::NoBrush);
        }
    }

    /* trigger instant marker at t=0 */
    if (trigView) {
        double x = xToPx(0.0, xMin, xMax, r);
        p.setPen(QPen(QColor(255,255,0,200), 1.5, Qt::DashLine));
        p.drawLine(QPointF(x, r.top()), QPointF(x, r.bottom()));
    }

    /* cursors A/B */
    if (gv->cursorsOn) {
        bool aBeyond = (gv->cursorA < xMin || gv->cursorA > xMax);
        bool bBeyond = (gv->cursorB < xMin || gv->cursorB > xMax);
        if (aBeyond && bBeyond) {
            gv->cursorA = xMin + (xMax-xMin)*0.25;
            gv->cursorB = xMin + (xMax-xMin)*0.75;
        }
        double xa = xToPx(gv->cursorA, xMin, xMax, r);
        double xb = xToPx(gv->cursorB, xMin, xMax, r);
        p.setPen(QPen(col::peach(), 1.2));
        p.drawLine(QPointF(xa, r.top()), QPointF(xa, r.bottom()));
        p.drawText(QPointF(xa+2, r.top()+12), "A");
        p.setPen(QPen(col::mauve(), 1.2));
        p.drawLine(QPointF(xb, r.top()), QPointF(xb, r.bottom()));
        p.drawText(QPointF(xb+2, r.top()+12), "B");
    }
    p.restore();

    /* update cursor readout label + HIST flag handled in PlotWidget::tick */
    w_->cursorLbl_->setVisible(gv->cursorsOn);
    if (gv->cursorsOn) {
        double dT = gv->cursorB - gv->cursorA;
        QString s = QString("dT=%1s  1/dT=%2Hz")
            .arg(fmtVal(dT))
            .arg(fmtVal(dT != 0.0 ? 1.0/std::fabs(dT) : 0.0));
        for (size_t si = 0; si < slots.size(); si++) {
            auto& a = slots[si];
            if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
            if (tStore[si].empty()) continue;
            const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
            double vA = sampleAt(tStore[si], vStore[si], gv->cursorA);
            double vB = sampleAt(tStore[si], vStore[si], gv->cursorB);
            s += QString("   %1: A=%2 B=%3 d=%4")
                 .arg(QString::fromStdString(sig.meta.name))
                 .arg(fmtVal(vA)).arg(fmtVal(vB)).arg(fmtVal(vB-vA));
        }
        w_->cursorLbl_->setText(s);
    }
}

/* ── interaction ─────────────────────────────────────────────────────────── */

void PlotCanvas::wheelEvent(QWheelEvent* e) {
    Hub* hub = w_->hub_;
    GlobalView* gv = w_->gv_;
    auto& slots = w_->slots_;
    const CaptureFrame* cap = hub->capture();
    const bool trigView = (cap != nullptr) && gv->trigView;
    bool& live = w_->live_;

    double dy = e->angleDelta().y();
    if (dy == 0.0) { e->ignore(); return; }
    const double factor = (dy > 0) ? 0.8 : 1.25;
    const bool ctrl  = e->modifiers() & Qt::ControlModifier;
    const bool shift = e->modifiers() & Qt::ShiftModifier;
    const double now = nowSec();

    auto enterTrigZoom = [&]() {
        if (trigView && !w_->trigZoomed_) {
            w_->setStoredX(-cap->preSec, cap->postSec);
            w_->trigZoomed_ = true;
        }
    };
    auto xZoomStored = [&](double f) {
        if (trigView) enterTrigZoom();
        if (now - w_->lastHistPushMs_ > 0.6) { w_->pushZoomHist(); w_->lastHistPushMs_ = now; }
        double cx   = (w_->plotXMin_ + w_->plotXMax_) * 0.5;
        double half = (w_->plotXMax_ - w_->plotXMin_) * 0.5 * f;
        w_->setStoredX(cx - half, cx + half);
    };

    auto makeManual = [&](PlotAssignment& a) {
        if (a.vs.mode != 2) {
            a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30);
            a.vs.offset   = a.vs.resolvedOffset;
            a.vs.mode     = 2;
        }
    };

    if (ctrl) {
        if (!trigView && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
        else xZoomStored(factor);
    } else if (shift) {
        if (w_->activeSlot_ >= 0 && w_->activeSlot_ < (int)slots.size()) {
            auto& a = slots[w_->activeSlot_];
            makeManual(a);
            a.vs.screenPos += (dy > 0) ? 0.5 : -0.5;
        }
    } else {
        if (w_->activeSlot_ >= 0 && w_->activeSlot_ < (int)slots.size()) {
            auto& a = slots[w_->activeSlot_];
            makeManual(a);
            a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
        } else {
            if (!trigView && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
            else xZoomStored(factor);
        }
    }
    update();
    e->accept();
}

void PlotCanvas::mousePressEvent(QMouseEvent* e) {
    GlobalView* gv = w_->gv_;
    const QRectF r = plotRect();
    lastPos_ = e->pos();
    if (e->button() == Qt::LeftButton && gv->cursorsOn) {
        double xa = xToPx(gv->cursorA, curXMin_, curXMax_, r);
        double xb = xToPx(gv->cursorB, curXMin_, curXMax_, r);
        if (std::fabs(e->pos().x() - xa) < 6) { dragCursor_ = 1; return; }
        if (std::fabs(e->pos().x() - xb) < 6) { dragCursor_ = 2; return; }
    }
    if (e->button() == Qt::RightButton) { panning_ = true; }
}

void PlotCanvas::mouseMoveEvent(QMouseEvent* e) {
    GlobalView* gv = w_->gv_;
    Hub* hub = w_->hub_;
    const QRectF r = plotRect();
    const CaptureFrame* cap = hub->capture();
    const bool trigView = (cap != nullptr) && gv->trigView;
    bool& live = w_->live_;

    if (dragCursor_ != 0) {
        double x = pxToX(e->pos().x(), curXMin_, curXMax_, r);
        if (dragCursor_ == 1) gv->cursorA = x; else gv->cursorB = x;
        update();
        return;
    }
    if (panning_) {
        if (trigView && !w_->trigZoomed_) {
            w_->setStoredX(-cap->preSec, cap->postSec);
            w_->trigZoomed_ = true;
        }
        if (!trigView && live) { w_->initPlotX(nowSec()); live = false; }
        double dxPix = e->pos().x() - lastPos_.x();
        lastPos_ = e->pos();
        double xRange = w_->plotXMax_ - w_->plotXMin_;
        if (r.width() > 0) {
            double dt = -dxPix / r.width() * xRange;
            w_->setStoredX(w_->plotXMin_ + dt, w_->plotXMax_ + dt);
        }
        update();
    }
}

void PlotCanvas::mouseReleaseEvent(QMouseEvent* e) {
    if (e->button() == Qt::RightButton) panning_ = false;
    if (e->button() == Qt::LeftButton)  dragCursor_ = 0;
}

void PlotCanvas::dragEnterEvent(QDragEnterEvent* e) {
    if (e->mimeData()->hasFormat(kMimeSignal)) e->acceptProposedAction();
}

void PlotCanvas::dropEvent(QDropEvent* e) {
    QByteArray d = e->mimeData()->data(kMimeSignal);
    if (d.size() >= (int)(2*sizeof(qint32))) {
        const qint32* p = reinterpret_cast<const qint32*>(d.constData());
        w_->addAssignment(p[0], p[1]);
        e->acceptProposedAction();
    }
}

/*===========================================================================*/
/* PlotWidget                                                                 */
/*===========================================================================*/

PlotWidget::PlotWidget(Hub* hub, GlobalView* gv, int plotIdx, QWidget* parent)
    : QWidget(parent), hub_(hub), gv_(gv), plotIdx_(plotIdx) {
    auto* lay = new QVBoxLayout(this);
    lay->setContentsMargins(2, 2, 2, 2);
    lay->setSpacing(2);

    header_ = new QWidget(this);
    headerLay_ = new QHBoxLayout(header_);
    headerLay_->setContentsMargins(2, 0, 2, 0);
    headerLay_->setSpacing(3);
    lay->addWidget(header_);

    cursorLbl_ = new QLabel(this);
    cursorLbl_->setStyleSheet("color:#fab387; font-size:10px;");
    cursorLbl_->setVisible(false);
    lay->addWidget(cursorLbl_);

    canvas_ = new PlotCanvas(this);
    lay->addWidget(canvas_, 1);

    connect(hub_, &Hub::zoomReceived,        this, &PlotWidget::onZoomReceived);
    connect(hub_, &Hub::historyZoomReceived, this, &PlotWidget::onHistoryZoomReceived);
    connect(hub_, &Hub::captureReceived,     this, &PlotWidget::onCaptureReceived);

    rebuildHeader();
}

void PlotWidget::addAssignment(int sourceIdx, int signalIdx) {
    for (const auto& a : slots_) {
        if (a.sourceIdx == sourceIdx && a.signalIdx == signalIdx) return;
    }
    PlotAssignment pa; pa.sourceIdx = sourceIdx; pa.signalIdx = signalIdx;
    slots_.push_back(pa);
    rebuildHeader();
    canvas_->update();
}

void PlotWidget::onModelChanged() { rebuildHeader(); }

void PlotWidget::onPauseChanged() {
    paused_ = gv_->paused;
    canvas_->update();
}

void PlotWidget::setLive(bool live) {
    live_ = live;
    if (live) { zoomHist_.clear(); hub_->zoomCache(plotIdx_).valid = false; }
    rebuildHeader();
    canvas_->update();
}

void PlotWidget::setStoredX(double mn, double mx) {
    if (mx > mn + 1e-12) { plotXMin_ = mn; plotXMax_ = mx; }
}

void PlotWidget::initPlotX(double tMax) {
    plotXMin_ = tMax - gv_->windowSec;
    plotXMax_ = tMax;
}

void PlotWidget::pushZoomHist() {
    if (!zoomHist_.empty() &&
        zoomHist_.back().first == plotXMin_ &&
        zoomHist_.back().second == plotXMax_) return;
    if (zoomHist_.size() >= 64) zoomHist_.erase(zoomHist_.begin());
    zoomHist_.emplace_back(plotXMin_, plotXMax_);
}

void PlotWidget::onZoomReceived(int p)        { if (p == plotIdx_) canvas_->update(); }
void PlotWidget::onHistoryZoomReceived(int p) { if (p == plotIdx_) canvas_->update(); }
void PlotWidget::onCaptureReceived() {
    trigZoomed_ = false; zoomHist_.clear(); canvas_->update();
}

/* ── periodic tick: repaint + throttled hi-res/history zoom requests ─────── */

void PlotWidget::tick() {
    Hub* hub = hub_;
    GlobalView* gv = gv_;
    const CaptureFrame* cap = hub->capture();
    const bool trigView = (cap != nullptr) && gv->trigView;
    const double now = nowSec();

    if (!trigView && !paused_) {
        std::string csv;
        for (const auto& a : slots_) {
            std::string k = hub->slotKey(a);
            if (k.empty()) continue;
            if (!csv.empty()) csv += ",";
            csv += k;
        }
        auto& zc = hub->zoomCache(plotIdx_);
        auto& hc = hub->histZoomCache(plotIdx_);
        if (live_ && gv->windowSec <= kLiveHiResMaxWin) {
            if (!csv.empty() && !zc.pending && now - lastLiveZoomMs_ > 0.25) {
                hub->requestZoom(plotIdx_, now - gv->windowSec, now, csv);
                lastLiveZoomMs_ = now;
            }
        } else if (!live_) {
            double t0 = plotXMin_, t1 = plotXMax_;
            if (t0 != lastT0_ || t1 != lastT1_) {
                lastT0_ = t0; lastT1_ = t1; rangeChangedMs_ = now;
            } else if (rangeChangedMs_ > 0.0 && now - rangeChangedMs_ > 0.35 && !csv.empty()) {
                if (!zc.pending && !(zc.valid && zc.t0 == t0 && zc.t1 == t1))
                    hub->requestZoom(plotIdx_, t0, t1, csv);
                const auto& hi = hub->historyInfo();
                if (hi.enabled && !hc.pending && !(hc.valid && hc.t0 == t0 && hc.t1 == t1))
                    hub->requestHistoryZoom(plotIdx_, t0, t1, csv);
                rangeChangedMs_ = 0.0;
            }
        }
    }
    canvas_->update();
}

/* ── header (badges + controls) ──────────────────────────────────────────── */

static void clearLayout(QLayout* lay) {
    QLayoutItem* it;
    while ((it = lay->takeAt(0)) != nullptr) {
        if (it->widget()) it->widget()->deleteLater();
        delete it;
    }
}

void PlotWidget::rebuildHeader() {
    clearLayout(headerLay_);
    auto& sources = hub_->sources();

    for (int i = 0; i < (int)slots_.size(); i++) {
        auto& a = slots_[i];
        if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
        if (a.signalIdx < 0 || a.signalIdx >= (int)sources[a.sourceIdx].signals.size()) continue;
        const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];

        auto* b = new QToolButton(header_);
        b->setCheckable(true);
        b->setChecked(activeSlot_ == i);
        b->setText(QString("%1  %2/div")
                   .arg(QString::fromStdString(sig.meta.name))
                   .arg(fmtVal(a.vs.resolvedDiv)));
        QColor c = sig.color;
        QString fg = (activeSlot_ == i) ? "#11111b" : "#11111b";
        QColor bg = (activeSlot_ == i) ? col::blue() : c;
        b->setStyleSheet(QString("QToolButton{background:%1;color:%2;border-radius:4px;"
                                 "padding:1px 6px;font-size:10px;font-weight:bold;}")
                         .arg(bg.name()).arg(fg));
        b->setContextMenuPolicy(Qt::CustomContextMenu);
        connect(b, &QToolButton::clicked, this, [this, i]() {
            activeSlot_ = (activeSlot_ == i) ? -1 : i;
            rebuildHeader(); canvas_->update();
        });
        connect(b, &QWidget::customContextMenuRequested, this, [this, i, b](const QPoint& pos) {
            showBadgeMenu(i, b->mapToGlobal(pos));
        });
        headerLay_->addWidget(b);
    }

    /* Live */
    auto* liveBtn = new QToolButton(header_);
    liveBtn->setText("Live");
    liveBtn->setCheckable(true);
    liveBtn->setChecked(live_);
    connect(liveBtn, &QToolButton::clicked, this, [this]() {
        setLive(true); paused_ = false;
    });
    headerLay_->addWidget(liveBtn);

    if (!live_ || (gv_->trigView && trigZoomed_)) {
        auto* back = new QToolButton(header_); back->setText("Back");
        back->setEnabled(!zoomHist_.empty());
        connect(back, &QToolButton::clicked, this, [this]() {
            if (!zoomHist_.empty()) {
                setStoredX(zoomHist_.back().first, zoomHist_.back().second);
                zoomHist_.pop_back(); rebuildHeader(); canvas_->update();
            }
        });
        headerLay_->addWidget(back);

        auto* fit = new QToolButton(header_);
        fit->setText(gv_->trigView ? "Reset" : "Fit");
        connect(fit, &QToolButton::clicked, this, [this]() {
            if (gv_->trigView) { trigZoomed_ = false; zoomHist_.clear(); }
            else {
                double mn = 1e300, mx = -1e300;
                auto& sources = hub_->sources();
                for (auto& a : slots_) {
                    if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
                    std::vector<double> tt, vv;
                    sources[a.sourceIdx].signals[a.signalIdx].buf.readLast(hub_->maxPoints(), tt, vv);
                    if (!tt.empty()) { mn = std::min(mn, tt.front()); mx = std::max(mx, tt.back()); }
                }
                if (mx > mn) { pushZoomHist(); setStoredX(mn, mx); }
            }
            rebuildHeader(); canvas_->update();
        });
        headerLay_->addWidget(fit);
    }

    /* N / D / M */
    const char* vl[3] = {"N", "D", "M"};
    for (int vm = 0; vm < 3; vm++) {
        auto* vb = new QToolButton(header_);
        vb->setText(vl[vm]);
        vb->setCheckable(true);
        vb->setChecked(vMode_ == vm);
        connect(vb, &QToolButton::clicked, this, [this, vm]() {
            vMode_ = vm; rebuildHeader(); canvas_->update();
        });
        headerLay_->addWidget(vb);
    }

    headerLay_->addStretch(1);
}

void PlotWidget::showBadgeMenu(int slotIdx, const QPoint& globalPos) {
    auto& sources = hub_->sources();
    if (slotIdx < 0 || slotIdx >= (int)slots_.size()) return;
    auto& a = slots_[slotIdx];
    if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) return;
    auto& sig = sources[a.sourceIdx].signals[a.signalIdx];

    QMenu m;
    m.addAction(QString::fromStdString(sig.meta.name))->setEnabled(false);
    m.addSeparator();

    m.addAction("Color…", [&]() {
        QColor c = QColorDialog::getColor(sig.color, this, "Trace color");
        if (c.isValid()) { sig.color = c; rebuildHeader(); canvas_->update(); }
    });

    QMenu* wm = m.addMenu("Width");
    for (double w : {0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0}) {
        wm->addAction(QString::number(w, 'f', 1), [&, w]() { sig.lineWidth = w; canvas_->update(); });
    }
    QMenu* mk = m.addMenu("Marker");
    const char* mn[] = {"None","Circle","Square","Diamond","Up","Down","Cross","Plus"};
    const int   mv[] = {-1, 0, 1, 2, 3, 4, 7, 8};
    for (int j = 0; j < 8; j++) {
        int val = mv[j];
        mk->addAction(mn[j], [&, val]() { sig.marker = val; canvas_->update(); });
    }
    if (vMode_ == 2) {
        QAction* dg = m.addAction("Digital (mixed)");
        dg->setCheckable(true); dg->setChecked(a.vs.digitalInMixed);
        connect(dg, &QAction::toggled, this, [&](bool on){ a.vs.digitalInMixed = on; canvas_->update(); });
    }

    m.addSeparator();
    QMenu* vs = m.addMenu("V-scale");
    const char* modes[] = {"Auto", "Range", "Manual"};
    for (int mm = 0; mm < 3; mm++) {
        QAction* act = vs->addAction(modes[mm]);
        act->setCheckable(true); act->setChecked(a.vs.mode == mm);
        connect(act, &QAction::triggered, this, [&, mm]() { a.vs.mode = mm; rebuildHeader(); canvas_->update(); });
    }
    vs->addSeparator();
    vs->addAction("Manual V/div…", [&]() {
        bool ok; double v = QInputDialog::getDouble(this, "V/div", "Units per division",
            a.vs.mode==2?a.vs.divValue:a.vs.resolvedDiv, -1e12, 1e12, 6, &ok);
        if (ok) { a.vs.divValue = v; a.vs.mode = 2; rebuildHeader(); canvas_->update(); }
    });
    vs->addAction("Offset…", [&]() {
        bool ok; double v = QInputDialog::getDouble(this, "Offset", "Center value",
            a.vs.mode==2?a.vs.offset:a.vs.resolvedOffset, -1e12, 1e12, 6, &ok);
        if (ok) { a.vs.offset = v; a.vs.mode = 2; rebuildHeader(); canvas_->update(); }
    });
    vs->addAction("Position (div)…", [&]() {
        bool ok; double v = QInputDialog::getDouble(this, "Position", "Divisions from center",
            a.vs.screenPos, -8, 8, 2, &ok);
        if (ok) { a.vs.screenPos = v; canvas_->update(); }
    });

    m.addSeparator();
    m.addAction("Remove from plot", [&]() {
        if (activeSlot_ == slotIdx) activeSlot_ = -1;
        else if (activeSlot_ > slotIdx) activeSlot_--;
        slots_.erase(slots_.begin() + slotIdx);
        rebuildHeader(); canvas_->update();
    });

    m.exec(globalPos);
}

} /* namespace shq */
