#include "plotwidget.h"
#include "lttb.h"

#include <QAction>
#include <QColorDialog>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMenu>
#include <QMimeData>
#include <QVBoxLayout>
#include <algorithm>
#include <cstring>

static const char* kMimeType = "application/x-signal";

static const QColor kPalette[] = {
    QColor(31,  119, 180),
    QColor(255, 127,  14),
    QColor(44,  160,  44),
    QColor(214,  39,  40),
    QColor(148, 103, 189),
    QColor(140,  86,  75),
    QColor(227, 119, 194),
    QColor(127, 127, 127),
    QColor(188, 189,  34),
    QColor(23,  190, 207),
};
static constexpr int kPaletteSize = static_cast<int>(sizeof(kPalette)/sizeof(kPalette[0]));
static int g_colorCounter = 0;

// ── PlotWidget ────────────────────────────────────────────────────────

PlotWidget::PlotWidget(int plotId, QWidget* parent)
    : QWidget(parent)
    , plotId_(plotId)
{
    auto* lay = new QVBoxLayout(this);
    lay->setContentsMargins(0,0,0,0);
    lay->setSpacing(0);

    qcp_ = new QCustomPlot(this);
    lay->addWidget(qcp_);

    applyDarkTheme();

    qcp_->setInteractions(
        QCP::iRangeDrag | QCP::iRangeZoom |
        QCP::iSelectLegend | QCP::iSelectPlottables);
    qcp_->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical);
    qcp_->axisRect()->setRangeZoom(Qt::Horizontal | Qt::Vertical);

    qcp_->legend->setVisible(true);
    qcp_->legend->setSelectableParts(QCPLegend::spItems);

    connect(qcp_, &QCustomPlot::legendDoubleClick,
            this, &PlotWidget::onLegendDoubleClick);
    connect(qcp_, &QCustomPlot::customContextMenuRequested,
            this, &PlotWidget::onContextMenuRequest);
    qcp_->setContextMenuPolicy(Qt::CustomContextMenu);

    // A/B cursor lines (owned by qcp_, initially invisible)
    cursorA_ = new QCPItemLine(qcp_);
    cursorA_->setVisible(false);
    cursorA_->setPen(QPen(QColor(255, 255, 0, 200), 1.5, Qt::DashLine));
    cursorA_->start->setType(QCPItemPosition::ptPlotCoords);
    cursorA_->end->setType(QCPItemPosition::ptPlotCoords);
    cursorA_->start->setCoords(0, -1e15);
    cursorA_->end->setCoords(0,   1e15);

    cursorB_ = new QCPItemLine(qcp_);
    cursorB_->setVisible(false);
    cursorB_->setPen(QPen(QColor(0, 230, 230, 200), 1.5, Qt::DashLine));
    cursorB_->start->setType(QCPItemPosition::ptPlotCoords);
    cursorB_->end->setType(QCPItemPosition::ptPlotCoords);
    cursorB_->start->setCoords(0, -1e15);
    cursorB_->end->setCoords(0,   1e15);

    // Detect user zoom/pan: any wheel event or drag → freeze range
    connect(qcp_, &QCustomPlot::mouseWheel, [this]() {
        if (!zoomGuard_) userZoomed_ = true;
    });
    connect(qcp_, &QCustomPlot::mouseRelease, [this](QMouseEvent* ev) {
        // Re-enable drag that may have been suppressed for Ctrl+click
        qcp_->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical);
        if (!zoomGuard_ && ev->button() == Qt::LeftButton)
            userZoomed_ = true;
    });

    // Ctrl+Left click → place cursor A;  Ctrl+Right click → place cursor B
    connect(qcp_, &QCustomPlot::mousePress, [this](QMouseEvent* ev) {
        if (!(ev->modifiers() & Qt::ControlModifier)) return;
        double x = qcp_->xAxis->pixelToCoord(ev->pos().x());
        if (ev->button() == Qt::LeftButton) {
            // Suppress drag so Ctrl+click only places the cursor
            qcp_->axisRect()->setRangeDrag(Qt::Orientations());
            emit cursorAPlaced(x);
        } else if (ev->button() == Qt::RightButton) {
            qcp_->axisRect()->setRangeDrag(Qt::Orientations());
            emit cursorBPlaced(x);
        }
    });

    // Emit xRangeChanged on user zoom/pan for cross-plot sync (D)
    connect(qcp_->xAxis,
            QOverload<const QCPRange&>::of(&QCPAxis::rangeChanged),
            [this](const QCPRange& r) {
                if (!zoomGuard_ && !syncGuard_)
                    emit xRangeChanged(r.lower, r.upper);
            });

    // Drop support
    setAcceptDrops(true);
    qcp_->setAcceptDrops(true);
    qcp_->installEventFilter(this);

    tmpT_.resize(kFullResPts);   // large enough for the full-res light path
    tmpV_.resize(kFullResPts);
    tmpMinV_.resize(kMaxBuckets);
    tmpMaxV_.resize(kMaxBuckets);
    outT_.resize(kMaxBuckets);
    outV_.resize(kMaxBuckets);
}

void PlotWidget::applyDarkTheme()
{
    QColor bg(20, 20, 20), fg(200, 200, 200), grid(60, 60, 60);

    qcp_->setBackground(QBrush(bg));
    qcp_->xAxis->setBasePen(QPen(fg));
    qcp_->yAxis->setBasePen(QPen(fg));
    qcp_->xAxis->setTickPen(QPen(fg));
    qcp_->yAxis->setTickPen(QPen(fg));
    qcp_->xAxis->setSubTickPen(QPen(fg));
    qcp_->yAxis->setSubTickPen(QPen(fg));
    qcp_->xAxis->setTickLabelColor(fg);
    qcp_->yAxis->setTickLabelColor(fg);
    qcp_->xAxis->setLabelColor(fg);
    qcp_->yAxis->setLabelColor(fg);
    qcp_->xAxis->grid()->setPen(QPen(grid, 0, Qt::DotLine));
    qcp_->yAxis->grid()->setPen(QPen(grid, 0, Qt::DotLine));
    qcp_->xAxis->grid()->setZeroLinePen(QPen(grid));
    qcp_->yAxis->grid()->setZeroLinePen(QPen(grid));
    qcp_->legend->setBrush(QBrush(QColor(30, 30, 30, 200)));
    qcp_->legend->setBorderPen(QPen(grid));
    qcp_->legend->setTextColor(fg);

    QSharedPointer<QCPAxisTickerTime> timeTicker(new QCPAxisTickerTime);
    timeTicker->setTimeFormat("%h:%m:%s");
    qcp_->xAxis->setTicker(timeTicker);
    qcp_->xAxis->setLabel("Time");
}

// ── Signal management ─────────────────────────────────────────────────

void PlotWidget::addSignal(std::shared_ptr<Source> src, int sigIdx)
{
    if (hasSignal(src, sigIdx)) return;
    Trace tr;
    tr.src    = std::move(src);
    tr.sigIdx = sigIdx;
    rebuildTrace(tr);
    traces_.push_back(std::move(tr));
}

void PlotWidget::removeSignal(std::shared_ptr<Source> src, int sigIdx)
{
    for (auto it = traces_.begin(); it != traces_.end(); ++it) {
        if (it->src == src && it->sigIdx == sigIdx) {
            if (it->graphMin)  qcp_->removeGraph(it->graphMin);
            if (it->graphMax)  qcp_->removeGraph(it->graphMax);
            if (it->graphLine) qcp_->removeGraph(it->graphLine);
            traces_.erase(it);
            qcp_->replot();
            return;
        }
    }
}

bool PlotWidget::hasSignal(const std::shared_ptr<Source>& src, int sigIdx) const
{
    for (const auto& tr : traces_)
        if (tr.src == src && tr.sigIdx == sigIdx) return true;
    return false;
}

void PlotWidget::rebuildTrace(Trace& tr)
{
    if (tr.graphMin)  { qcp_->removeGraph(tr.graphMin);  tr.graphMin  = nullptr; }
    if (tr.graphMax)  { qcp_->removeGraph(tr.graphMax);  tr.graphMax  = nullptr; }
    if (tr.graphLine) { qcp_->removeGraph(tr.graphLine); tr.graphLine = nullptr; }

    // Use stored user color if set, otherwise pick from palette
    if (!tr.color.isValid())
        tr.color = kPalette[g_colorCounter++ % kPaletteSize];

    std::string name;
    {
        std::lock_guard<std::mutex> lk(tr.src->signals_mutex);
        if (tr.sigIdx < static_cast<int>(tr.src->signal_states.size()))
            name = tr.src->signal_states[tr.sigIdx].def.name;
    }

    tr.graphMin  = qcp_->addGraph();
    tr.graphMax  = qcp_->addGraph();
    tr.graphLine = qcp_->addGraph();

    tr.graphMin->setPen(Qt::NoPen);
    tr.graphMin->setName("");
    tr.graphMin->removeFromLegend();

    QColor fillCol(tr.color); fillCol.setAlpha(50);
    tr.graphMax->setPen(Qt::NoPen);
    tr.graphMax->setBrush(QBrush(fillCol));
    tr.graphMax->setChannelFillGraph(tr.graphMin);
    tr.graphMax->setName("");
    tr.graphMax->removeFromLegend();

    tr.graphLine->setPen(QPen(tr.color, tr.lineWidth));
    tr.graphLine->setName(QString::fromStdString(name));
    tr.graphLine->setAdaptiveSampling(false);
    tr.lastHead = 0;
}

// ── View control ──────────────────────────────────────────────────────

void PlotWidget::setViewRange(double tMin, double tMax)
{
    userZoomed_ = true;
    zoomGuard_ = true;
    qcp_->xAxis->setRange(tMin, tMax);
    zoomGuard_ = false;
}

void PlotWidget::resetZoom()
{
    userZoomed_ = false;
}

void PlotWidget::applyZoomRange(double lo, double hi)
{
    syncGuard_ = true;
    zoomGuard_ = true;
    userZoomed_ = true;
    qcp_->xAxis->setRange(lo, hi);
    zoomGuard_ = false;
    syncGuard_ = false;
    qcp_->replot(QCustomPlot::rpQueuedReplot);
}

void PlotWidget::fitYAxis()
{
    qcp_->yAxis->rescale(true);
    qcp_->replot(QCustomPlot::rpQueuedReplot);
}

QPair<double,double> PlotWidget::currentXRange() const
{
    return { qcp_->xAxis->range().lower, qcp_->xAxis->range().upper };
}

// ── Refresh (called each timer tick) ─────────────────────────────────

void PlotWidget::refresh(double viewTMax, double windowSec, bool paused)
{
    if (traces_.empty()) return;

    bool useLiveRange = !paused && !userZoomed_;

    double tMin, tMax;
    if (useLiveRange) {
        tMax = viewTMax;
        tMin = tMax - windowSec;
    } else {
        tMin = qcp_->xAxis->range().lower;
        tMax = qcp_->xAxis->range().upper;
    }

    // Adaptive display target: 2× pixel width, clamped to [300, kMaxBuckets]
    int pixW = qcp_->viewport().width();
    size_t displayTarget = static_cast<size_t>(
        std::max(300, std::min(pixW * 2, static_cast<int>(kMaxBuckets))));

    bool anyNewData   = false;
    double tMinActual = tMax;

    for (auto& tr : traces_) {
        if (!tr.graphLine) continue;

        std::shared_ptr<SignalBuffer> buf, arc;
        size_t curHead = 0;
        {
            std::lock_guard<std::mutex> lk(tr.src->signals_mutex);
            if (tr.sigIdx >= static_cast<int>(tr.src->signal_states.size())) continue;
            const auto& ss = tr.src->signal_states[tr.sigIdx];
            buf    = ss.buffer;
            arc    = ss.archive;
            curHead = buf ? buf->total_written() : 0;
        }
        if (!buf) continue;

        // Select buffer: fall back to archive ONLY when the buffer has zero
        // coverage of the current window (user scrolled past the ring buffer).
        SignalBuffer* use = buf.get();
        double buf_oldt = buf->count() > 0 ? buf->oldest_time() : tMax + 1.0;
        if (arc && arc->count() > 0 && buf_oldt >= tMax)
            use = arc.get();

        // Track oldest available sample to adapt x range for new signals
        double oldest = use->oldest_time();
        if (oldest > 0.0 && oldest < tMinActual)
            tMinActual = oldest;

        // Dirty check: skip reprocessing when paused+zoomed and nothing changed
        bool rangeChanged = (tr.lastTMin != tMin || tr.lastTMax != tMax);
        bool dataChanged  = (curHead != tr.lastHead);
        if (!useLiveRange && !dataChanged && !rangeChanged) {
            if (tr.graphLine->data()->size() > 0) {
                anyNewData = true;
                continue;
            }
        }

        // Count samples in view window — cheap O(log N), drives path selection
        size_t countInWin = use->count_in_window(tMin, tMax);
        if (countInWin == 0) {
            tr.graphLine->data()->clear();
            tr.graphMin->data()->clear();
            tr.graphMax->data()->clear();
            tr.lastHead = curHead;
            tr.lastTMin = tMin;
            tr.lastTMax = tMax;
            continue;
        }

        // ── Adaptive rendering path ───────────────────────────────────
        //
        //  Light   (<= kFullResPts samples):
        //    read_window → up to kFullResPts pts → LTTB → graphLine only
        //    The threshold equals the buffer size so read_window never
        //    truncates (every sample in the window is read).
        //
        //  Moderate (kFullResPts .. kModerateThreshold):
        //    read_window_minmax → kMaxBuckets buckets → envelope
        //    graphMin=min, graphMax=max (filled), graphLine=midpoint
        //
        //  Heavy   (> kModerateThreshold):
        //    read_window_decimated → kRawPts pts → LTTB → graphLine only
        //    (fastest path; avoids scanning millions of points)

        static constexpr size_t kModerateThreshold = 500'000;
        bool useEnvelope = false;
        size_t nPts = 0;

        if (countInWin <= kFullResPts) {
            // Light: full-resolution, no truncation, LTTB to displayTarget
            nPts = use->read_window(tMin, tMax, tmpT_.data(), tmpV_.data(), kFullResPts);

        } else if (countInWin <= kModerateThreshold) {
            // Moderate: min-max decimation — preserves peaks, good quality
            nPts = use->read_window_minmax(tMin, tMax,
                                           tmpT_.data(), tmpMinV_.data(), tmpMaxV_.data(),
                                           kMaxBuckets);
            useEnvelope = (nPts > 0);

        } else {
            // Heavy: uniform stride to kRawPts, then LTTB
            nPts = use->read_window_decimated(tMin, tMax,
                                              tmpT_.data(), tmpV_.data(), kRawPts);
        }

        if (nPts == 0) {
            tr.graphLine->data()->clear();
            tr.graphMin->data()->clear();
            tr.graphMax->data()->clear();
            tr.lastHead = curHead;
            tr.lastTMin = tMin;
            tr.lastTMax = tMax;
            continue;
        }

        if (tmpT_[0] < tMinActual) tMinActual = tmpT_[0];

        anyNewData = true;
        tr.lastHead = curHead;
        tr.lastTMin = tMin;
        tr.lastTMax = tMax;

        if (useEnvelope) {
            // Envelope path: graphMin=lower bound, graphMax=upper bound (filled),
            // graphLine=midpoint.  All three arrays are displayTarget-sized max.
            QVector<double> xs(static_cast<int>(nPts));
            QVector<double> yMin(static_cast<int>(nPts));
            QVector<double> yMax(static_cast<int>(nPts));
            QVector<double> yMid(static_cast<int>(nPts));
            for (size_t i = 0; i < nPts; ++i) {
                xs[static_cast<int>(i)]   = tmpT_[i];
                yMin[static_cast<int>(i)] = tmpMinV_[i];
                yMax[static_cast<int>(i)] = tmpMaxV_[i];
                yMid[static_cast<int>(i)] = 0.5 * (tmpMinV_[i] + tmpMaxV_[i]);
            }
            tr.graphMin->setData(xs, yMin, true);
            tr.graphMax->setData(xs, yMax, true);
            tr.graphLine->setData(xs, yMid, true);

        } else {
            // Line path: LTTB → graphLine only, clear envelope
            if (outT_.size() < displayTarget) {
                outT_.resize(displayTarget);
                outV_.resize(displayTarget);
            }
            size_t nDisp = lttb(tmpT_.data(), tmpV_.data(), nPts,
                                outT_.data(), outV_.data(), displayTarget);

            QVector<double> xs(outT_.data(), outT_.data() + static_cast<int>(nDisp));
            QVector<double> ys(outV_.data(), outV_.data() + static_cast<int>(nDisp));
            tr.graphLine->setData(xs, ys, true);
            tr.graphMin->data()->clear();
            tr.graphMax->data()->clear();
        }
    }

    if (!anyNewData) return;

    // Update x-axis range
    if (useLiveRange) {
        double displayMin = (tMinActual < tMax && tMinActual > tMin)
            ? tMinActual : tMin;
        zoomGuard_ = true;
        qcp_->xAxis->setRange(displayMin, tMax);
        zoomGuard_ = false;
        qcp_->yAxis->rescale(true);
    }

    qcp_->replot(QCustomPlot::rpQueuedReplot);
}

// ── Drag & Drop ───────────────────────────────────────────────────────

void PlotWidget::dragEnterEvent(QDragEnterEvent* e)
{
    if (e->mimeData()->hasFormat(kMimeType)) e->acceptProposedAction();
    else e->ignore();
}

void PlotWidget::dropEvent(QDropEvent* e)
{
    if (!e->mimeData()->hasFormat(kMimeType)) { e->ignore(); return; }
    QByteArray ba = e->mimeData()->data(kMimeType);
    if (ba.size() < 8) { e->ignore(); return; }
    int srcIdx = 0, sigIdx = 0;
    std::memcpy(&srcIdx, ba.data(),     4);
    std::memcpy(&sigIdx, ba.data() + 4, 4);
    emit signalDropped(srcIdx, sigIdx);
    e->acceptProposedAction();
}

// ── Context menu ──────────────────────────────────────────────────────

void PlotWidget::onContextMenuRequest(QPoint pos)
{
    // Ctrl+Right click places cursor B — don't open the context menu
    if (QApplication::keyboardModifiers() & Qt::ControlModifier) return;

    QMenu menu(this);

    // ── View actions ────────────────────────────────────────────────
    if (userZoomed_) {
        auto* act = menu.addAction("↩  Reset Zoom (Live)");
        connect(act, &QAction::triggered, this, &PlotWidget::resetZoom);
    }
    auto* fitAct = menu.addAction("⤢  Fit Y Axis");
    connect(fitAct, &QAction::triggered, this, &PlotWidget::fitYAxis);

    // ── Per-signal style submenus ───────────────────────────────────
    if (!traces_.empty()) menu.addSeparator();

    for (size_t i = 0; i < traces_.size(); ++i) {
        std::string name;
        {
            std::lock_guard<std::mutex> lk(traces_[i].src->signals_mutex);
            if (traces_[i].sigIdx < static_cast<int>(traces_[i].src->signal_states.size()))
                name = traces_[i].src->signal_states[traces_[i].sigIdx].def.name;
        }
        auto* sub = menu.addMenu(QString::fromStdString(name));

        // Color picker
        auto* colorAct = sub->addAction("Color…");
        connect(colorAct, &QAction::triggered, [this, i]() {
            if (i >= traces_.size()) return;
            QColor c = QColorDialog::getColor(traces_[i].color, this, "Signal Color");
            if (!c.isValid()) return;
            traces_[i].color = c;
            if (traces_[i].graphLine)
                traces_[i].graphLine->setPen(QPen(c, traces_[i].lineWidth));
            QColor fc(c); fc.setAlpha(50);
            if (traces_[i].graphMax) traces_[i].graphMax->setBrush(QBrush(fc));
            qcp_->replot(QCustomPlot::rpQueuedReplot);
        });

        // Line width submenu
        auto* widthMenu = sub->addMenu("Line Width");
        static const qreal kWidths[] = { 0.5, 1.0, 1.5, 2.0, 2.5, 3.0 };
        for (qreal w : kWidths) {
            auto* wa = widthMenu->addAction(QString::number(w, 'f', 1) + " px");
            wa->setCheckable(true);
            wa->setChecked(qFuzzyCompare(traces_[i].lineWidth, w));
            connect(wa, &QAction::triggered, [this, i, w]() {
                if (i >= traces_.size()) return;
                traces_[i].lineWidth = w;
                if (traces_[i].graphLine)
                    traces_[i].graphLine->setPen(QPen(traces_[i].color, w));
                qcp_->replot(QCustomPlot::rpQueuedReplot);
            });
        }

        sub->addSeparator();
        auto* removeAct = sub->addAction("Remove");
        connect(removeAct, &QAction::triggered, [this, i]() {
            if (i < traces_.size())
                removeSignal(traces_[i].src, traces_[i].sigIdx);
        });
    }

    if (!menu.isEmpty()) menu.exec(qcp_->mapToGlobal(pos));
}

void PlotWidget::onLegendDoubleClick(QCPLegend*, QCPAbstractLegendItem* item)
{
    if (auto* pli = qobject_cast<QCPPlottableLegendItem*>(item)) {
        if (auto* g = qobject_cast<QCPGraph*>(pli->plottable())) {
            for (auto& tr : traces_) {
                if (tr.graphLine == g) { removeSignal(tr.src, tr.sigIdx); return; }
            }
        }
    }
}

// ── Cross-plot X-axis sync (D) ─────────────────────────────────────────

void PlotWidget::syncXRange(double lo, double hi)
{
    if (syncGuard_) return;
    syncGuard_ = true;
    zoomGuard_ = true;
    userZoomed_ = true;   // freeze this plot too
    qcp_->xAxis->setRange(lo, hi);
    zoomGuard_ = false;
    syncGuard_ = false;
    qcp_->replot(QCustomPlot::rpQueuedReplot);
}

// ── A/B cursors (C) ───────────────────────────────────────────────────

void PlotWidget::refreshCursors(double tA, double tB, bool aActive, bool bActive)
{
    cursorATime_   = tA; cursorAActive_ = aActive;
    cursorBTime_   = tB; cursorBActive_ = bActive;
    updateCursorItems();
    qcp_->replot(QCustomPlot::rpQueuedReplot);
}

void PlotWidget::updateCursorItems()
{
    if (cursorA_) {
        cursorA_->setVisible(cursorAActive_);
        if (cursorAActive_) {
            cursorA_->start->setCoords(cursorATime_, -1e15);
            cursorA_->end->setCoords(cursorATime_,   1e15);
        }
    }
    if (cursorB_) {
        cursorB_->setVisible(cursorBActive_);
        if (cursorBActive_) {
            cursorB_->start->setCoords(cursorBTime_, -1e15);
            cursorB_->end->setCoords(cursorBTime_,   1e15);
        }
    }
}

std::vector<PlotWidget::SignalId> PlotWidget::getSignals() const
{
    std::vector<SignalId> result;
    result.reserve(traces_.size());
    for (const auto& tr : traces_)
        result.push_back({tr.src, tr.sigIdx});
    return result;
}

bool PlotWidget::eventFilter(QObject* obj, QEvent* ev)
{
    if (obj == qcp_) {
        if (ev->type() == QEvent::DragEnter) {
            dragEnterEvent(static_cast<QDragEnterEvent*>(ev)); return true;
        }
        if (ev->type() == QEvent::Drop) {
            dropEvent(static_cast<QDropEvent*>(ev)); return true;
        }
        if (ev->type() == QEvent::DragMove) {
            static_cast<QDragMoveEvent*>(ev)->acceptProposedAction(); return true;
        }
    }
    return QWidget::eventFilter(obj, ev);
}
