#include <QColorDialog>
#include <QDebug>
#include <QFileDialog>
#include <QGroupBox>
#include <QInputDialog>
#include <QSettings>
#include <QSpinBox>
#include <iostream>
#include <math.h>

#include "ConfigPanel.h"
#include "GraphLineStyleDialog.h"
#include "GraphView.h"
#include "OriginTimestamp.h"
#include "TDeviceManager.h"
#include "timestamp.h"

#define DEFAULT_GRAPH_LINE_WIDTH 2
#define DEFAULT_XAXIS_LABEL "Time"
#define DEFAULT_YAXIS_LABEL "Value"
#define DEFAULT_TITLE "Untitled Graph"

GraphView::GraphView() : origin(OriginTimestamp::instance().value()) {
    QSettings settings;

    // Remove old deprecated keys
    settings.remove("graph/sample_interval_ms");

    QVBoxLayout *lay = new QVBoxLayout();
    this->setLayout(lay);

    isPaused = 0;

    plot = new QCustomPlot(this);

    plot->xAxis->setLabel(settings.value("graph/xaxis_label", DEFAULT_XAXIS_LABEL).toString());
    plot->yAxis->setLabel(settings.value("graph/yaxis_label", DEFAULT_YAXIS_LABEL).toString());
    plot->axisRect()->setRangeDrag(Qt::Horizontal);
    plot->axisRect()->setRangeZoom(Qt::Horizontal);
    plot->setInteractions(QCP::iRangeZoom | QCP::iRangeDrag);

    connect(plot, SIGNAL(legendClick(QCPLegend *, QCPAbstractLegendItem *, QMouseEvent *)), this,
            SLOT(editLegend(QCPLegend *, QCPAbstractLegendItem *, QMouseEvent *)));
    connect(plot, SIGNAL(axisClick(QCPAxis *, QCPAxis::SelectablePart, QMouseEvent *)), this,
            SLOT(editAxis(QCPAxis *, QCPAxis::SelectablePart, QMouseEvent *)));

    QSharedPointer<QCPAxisTickerDateTime> timeTicker(new QCPAxisTickerDateTime);
    timeTicker->setDateTimeFormat("H:mm:ss.z\nddd M/dd");
    plot->xAxis->setTicker(timeTicker);

    // Default axis ranges
    xMin = timestamp_to_double(OriginTimestamp::instance().value());
    xMax = xMin + 60.0; // 1 minute
    xBoundMin = xMin;
    xBoundMax = xMax;

    plot->xAxis->setRange(xMin, xMax);
    plot->yAxis->setRange(0.0, 120.0);

    QFont legendFont = font();
    legendFont.setPointSize(9);
    plot->legend->setVisible(true);
    plot->legend->setFont(legendFont);
    plot->axisRect()->insetLayout()->setInsetPlacement(0, QCPLayoutInset::ipBorderAligned);
    plot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignBottom | Qt::AlignRight);

    plot->setAutoAddPlottableToLegend(true);

    // Title

    title = new QCPTextElement(plot, settings.value("graph/title", DEFAULT_TITLE).toString());
    QFont font = title->font();
    font.setBold(true);
    font.setPointSize(font.pointSize() + 2);
    title->setFont(font);

    plot->plotLayout()->insertRow(0);
    plot->plotLayout()->addElement(0, 0, title);

    connect(title, &QCPTextElement::clicked, this, &GraphView::editTitle);

    plot->setNoAntialiasingOnDrag(true);
    plot->setNotAntialiasedElements(QCP::aeAll);

    //////////////////////////
    QGroupBox *graph_opts = new QGroupBox(tr("Operations"));
    graph_opts->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
    QHBoxLayout *graph_opts_lay = new QHBoxLayout();
    graph_opts->setLayout(graph_opts_lay);

    PauseResumeButton = new QPushButton(tr("Pause"));
    connect(PauseResumeButton, SIGNAL(clicked()), this, SLOT(pauseUnpause()));

    QPushButton *btn_reset = new QPushButton(tr("Reset graph"));
    connect(btn_reset, SIGNAL(clicked()), this, SLOT(resetGraph()));

    QPushButton *btn_save = new QPushButton(QIcon(":fileopen.png"), tr("Save graph to file..."));
    connect(btn_save, SIGNAL(clicked()), this, SLOT(saveGraph()));

    graph_opts_lay->addWidget(PauseResumeButton);
    graph_opts_lay->addWidget(btn_reset);
    graph_opts_lay->addWidget(btn_save);
    graph_opts_lay->addStretch();

    ////////////////////////// OPTIONS
    QGroupBox *graph_opts2 = new QGroupBox(tr("Options"));
    graph_opts2->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

    QVBoxLayout *graph_opts_vbox = new QVBoxLayout();
    graph_opts_vbox->setContentsMargins(0, 0, 0, 0);
    graph_opts2->setLayout(graph_opts_vbox);

    QHBoxLayout *graph_opts_lay2 = new QHBoxLayout();
    QHBoxLayout *graph_opts_lay3 = new QHBoxLayout();
    graph_opts_lay2->setContentsMargins(0, 0, 0, 0);
    graph_opts_lay3->setContentsMargins(0, 0, 0, 0);

    QFrame *line1 = new QFrame();
    line1->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
    line1->setLayout(graph_opts_lay2);

    QFrame *line2 = new QFrame();
    line2->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
    line2->setLayout(graph_opts_lay3);

    graph_opts_vbox->addWidget(line1);
    graph_opts_vbox->addWidget(line2);

    // Buffer capacity

    int days = settings.value("graph/buffer_capacity_days", 2).toInt();
    refreshBounds(days);

    graph_opts_lay2->addWidget(new QLabel("Buffer capacity: "));
    bufferSpinBox = new QSpinBox();
    bufferSpinBox->setMinimum(1);
    bufferSpinBox->setMaximum(365);
    bufferSpinBox->setMinimumWidth(bufferSpinBox->sizeHint().width() + 10);
    bufferSpinBox->setValue(days);
    bufferSpinBox->setSuffix(days > 1 ? " days" : "day");
    graph_opts_lay2->addWidget(bufferSpinBox);
    connect(bufferSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onBufferSizeSelected(int)));

    graph_opts_lay2->addStretch();

    bufferSpinBoxTimer = new QTimer();
    bufferSpinBoxTimer->setSingleShot(true);
    bufferSpinBoxTimer->setInterval(500);
    connect(bufferSpinBoxTimer, SIGNAL(timeout()), this, SLOT(applyBufferSize()));

    // Interval

    graph_opts_lay2->addWidget(new QLabel("Interval: "));
    graphIntervalSpinBox = new QDoubleSpinBox();
    graphIntervalSpinBox->setSuffix(" s");
    graphIntervalSpinBox->setDecimals(3);
    graph_opts_lay2->addWidget(graphIntervalSpinBox);
    graph_opts_lay2->addWidget(new QLabel(" (multiple of global sampling interval)"));
    graph_opts_lay2->addStretch();
    connect(graphIntervalSpinBox, SIGNAL(valueChanged(double)), this, SLOT(onGraphIntervalSelected(double)));

    graphIntervalSpinBoxTimer = new QTimer();
    graphIntervalSpinBoxTimer->setSingleShot(true);
    graphIntervalSpinBoxTimer->setInterval(500);
    connect(graphIntervalSpinBoxTimer, SIGNAL(timeout()), this, SLOT(applyGraphInterval()));

    // Axis options

    xAutoScaleCheckBox = new ConfigCheckBox(tr("Auto-scale X axis"), "graph/autoscale_x", true);
    yAutoScaleCheckBox = new ConfigCheckBox(tr("Auto-scale Y axis"), "graph/autoscale_y", true);
    yLogScaleCheckBox = new ConfigCheckBox(tr("Logarithmic Y axis"), "graph/log_y");

    connect(xAutoScaleCheckBox, SIGNAL(changed(bool)), this, SLOT(onAutoScaleChanged(bool)));
    connect(yAutoScaleCheckBox, SIGNAL(changed(bool)), this, SLOT(onAutoScaleChanged(bool)));
    connect(yLogScaleCheckBox, SIGNAL(changed(bool)), this, SLOT(onYLogScaleChanged(bool)));
    graph_opts_lay2->addWidget(xAutoScaleCheckBox);
    graph_opts_lay2->addWidget(yAutoScaleCheckBox);
    graph_opts_lay2->addWidget(yLogScaleCheckBox);

    // Memory usage

    graph_opts_lay3->addWidget(new QLabel("Estimated memory usage: "));
    memoryEstimateLabel = new QLabel();
    graph_opts_lay3->addWidget(memoryEstimateLabel);
    graph_opts_lay3->addStretch();
    graphIntervalWarningLabel = new QLabel("Interval was automatically increased due to excessive computing load ");
    graphIntervalWarningLabel->setStyleSheet("color: red;");
    graphIntervalWarningLabel->setVisible(false);
    graph_opts_lay3->addWidget(graphIntervalWarningLabel);
    graphIntervalWarningButton = new QPushButton(" X ");
    graphIntervalWarningButton->setStyleSheet("color: red; border: 1px solid red;");
    graphIntervalWarningButton->setVisible(false);
    connect(graphIntervalWarningButton, SIGNAL(clicked()), this, SLOT(hideIntervalWarning()));
    graph_opts_lay3->addWidget(graphIntervalWarningButton);
    graph_opts_lay3->addStretch();

    // Line quality

    graphQualityPref = new GraphQualityPreference();
    connect(graphQualityPref, SIGNAL(changed()), this, SLOT(onGraphQualityChanged()));

    graph_opts_lay3->addWidget(new QLabel(tr("Line quality:")));
    graph_opts_lay3->addWidget(graphQualityPref);

    // Graph legend

    graphLegendPref = new GraphLegendPreference();
    connect(graphLegendPref, SIGNAL(changed()), this, SLOT(onGraphLegendChanged()));

    graph_opts_lay3->addWidget(new QLabel(tr("Graph legend:")));
    graph_opts_lay3->addWidget(graphLegendPref);
    // graph_opts_lay3->addStretch();

    // Construct main layout

    lay->addWidget(plot);
    lay->addWidget(graph_opts2);
    lay->addWidget(graph_opts);

    plot->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    graph_opts->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

    TDeviceManager &dm = TDeviceManager::instance();
    connect(&dm, &TDeviceManager::channelAliasUpdated, this, &GraphView::onChannelAliasUpdated);
    dm.addWatchdogListener(this);

    // Initialize private members

    graphIntervalMultiple = settings.value("graph/sample_interval_multiple", 1).toInt();

    graphInterval = -1;
    setGlobalInterval(ConfigPanel::instance().getInterval());

    connect(&(ConfigPanel::instance()), &ConfigPanel::intervalChanged, this, &GraphView::setGlobalInterval);

    // Initialize graph parameters
    plot->setPlottingHints(QCP::phCacheLabels | QCP::phImmediateRefresh);
    onAutoScaleChanged();
    onYLogScaleChanged();
    onGraphQualityChanged();
    onGraphLegendChanged();
}

GraphView::~GraphView() {}

void GraphView::editAxis(QCPAxis *axis, QCPAxis::SelectablePart part, QMouseEvent *event) {
    QSettings settings;
    QString new_name;
    bool ok = false;

    // Silence unused warning
    (void)part;
    (void)event;

    new_name = QInputDialog::getText(this, tr("Enter axis label"), tr("Label:"), QLineEdit::Normal, axis->label(), &ok);

    new_name = new_name.trimmed();
    if (ok && !new_name.isEmpty()) {
        axis->setLabel(new_name);
        replot();

        if (axis == plot->xAxis) {
            settings.setValue("graph/xaxis_label", new_name);
        } else if (axis == plot->yAxis) {
            settings.setValue("graph/yaxis_label", new_name);
        }
    }
}

void GraphView::editLegend(QCPLegend *legend, QCPAbstractLegendItem *item, QMouseEvent *event) {
    (void)legend;
    (void)event;

    if (!item)
        return;

    int index = -1;

    // plot->legend (QCPLegend*)
    for (int i = 0; i < plot->legend->itemCount(); i++) {
        if (item == plot->legend->item(i)) {
            index = i;
            break;
        }
    }

    if (index < 0) {
        return;
    }

    QCPGraph *gr = plot->graph(index);
    if (!gr) {
        return;
    }

    GraphLineStyleDialog *ls_dialog = new GraphLineStyleDialog();
    ls_dialog->setCurrentSettings(gr->pen());
    ls_dialog->exec();

    if (ls_dialog->apply) {
        QPen pen = ls_dialog->getCurrentSettings();
        gr->setPen(pen);
        const QString &id = channelIdByGraph.value(gr);
        QSettings settings;
        QString key = "graphStyle/" + id + "pen";
        settings.setValue(key, pen);
    }

    delete ls_dialog;
}

void GraphView::editTitle() {
    QSettings settings;
    QString new_title;
    bool ok = false;

    new_title =
        QInputDialog::getText(this, tr("Enter graph title"), tr("Title:"), QLineEdit::Normal, title->text(), &ok);

    new_title = new_title.trimmed();
    if (ok && !new_title.isEmpty()) {
        title->setText(new_title);
        replot();

        settings.setValue("graph/title", new_title);
    }
}

void GraphView::saveGraph(void) {
    QString filename;
    QString default_dir = QDir::homePath();
    QString sel_filt;

    filename = QFileDialog::getSaveFileName(this, tr("Save graph to file"), default_dir,
                                            "PNG (*.png);; JPEG (*.jpg *.jpeg);; BMP (*.bmp);; PDF (*.pdf)", &sel_filt);

    if (filename.size()) {

        if (sel_filt.startsWith("JPEG")) {
            QFileInfo inf(filename);
            if (!inf.suffix().toLower().startsWith("jp")) {
                filename = filename + ".jpeg";
            }
            plot->saveJpg(filename);
        }
        if (sel_filt.startsWith("PNG")) {
            QFileInfo inf(filename);
            if (inf.suffix().toLower().compare("png") != 0) {
                filename = filename + ".png";
            }

            plot->savePng(filename);
        }
        if (sel_filt.startsWith("BMP")) {
            QFileInfo inf(filename);
            if (inf.suffix().toLower().compare("bmp") != 0) {
                filename = filename + ".bmp";
            }

            plot->saveBmp(filename);
        }
        if (sel_filt.startsWith("PDF")) {
            QFileInfo inf(filename);
            if (inf.suffix().toLower().compare("pdf") != 0) {
                filename = filename + ".pdf";
            }

            plot->savePdf(filename, true);
        }
    }
}

QCPGraph *GraphView::addGraph(const QString &channelId) {
    QCPGraph *graph = plot->addGraph();

    // Determine name

    const QString &alias = TDeviceManager::instance().getAlias(channelId);
    const QString &name = alias.isEmpty() ? channelId : alias;
    graph->setName(name);

    // Determine pen

    int index = channelIdByGraph.size();

#define NUM_DEFAULT_COLORS 12
    QColor colors[NUM_DEFAULT_COLORS] = {
        Qt::green,
        Qt::blue,
        Qt::red,
        QColor(255, 133, 26), // orange
        Qt::cyan,
        Qt::magenta,
        QColor(255, 200, 255), // pink
        Qt::darkMagenta,
        Qt::darkRed,
        Qt::darkYellow,
        Qt::gray,
        Qt::black,
    };

    QSettings settings;
    QString key = "graphStyle/" + channelId + "pen";

    // If a setting does not already exist, choose a color
    // from the palete above, and create the setting.
    QVariant v = settings.value(key);
    if (v.isNull()) {
        QPen pen(colors[index % NUM_DEFAULT_COLORS]);
        pen.setWidth(DEFAULT_GRAPH_LINE_WIDTH);
        settings.setValue(key, pen);
    }

    // Now load the setting created above, or a pre-existing setting.
    QPen pen = settings.value(key).value<QPen>();
    graph->setPen(pen);

    return graph;
}

void GraphView::resetGraph(void) {
    QMutableMapIterator<QString, QCPGraph *> it(graphByChannelId);

    while (it.hasNext()) {
        it.next();
        QCPGraph *graph = it.value();
        if (graph->visible()) {
            graph->data()->clear();
        } else {
            plot->removeGraph(graph);
            it.remove();
            channelIdByGraph.remove(graph);
        }
    }

    replot();
}

void GraphView::addChannel(const QString &id) {
    QCPGraph *graph = graphByChannelId.value(id);

    if (graph) {
        if (graph->visible()) {
            return;
        }
    } else {
        graph = addGraph(id);
        graphByChannelId.insert(id, graph);
        channelIdByGraph.insert(graph, id);
    }

    TChannel *channel = TDeviceManager::instance().getChannel(id);
    if (!channel) {
        return;
    }

    TDevice *device = channel->device;
    int n = deviceTicker.value(device->serialNumber);

    if (n == 0) {
        // first time we encounter this device
        TDeviceSignal *signal = TDeviceManager::instance().getDeviceSignal(device->serialNumber);
        if (signal) {
            signal->connect(signal, &TDeviceSignal::deviceConnected, this, &GraphView::update, Qt::UniqueConnection);
            signal->connect(signal, &TDeviceSignal::deviceDisconnected, this, &GraphView::update, Qt::UniqueConnection);
            signal->connect(signal, &TDeviceSignal::deviceUpdated, this, &GraphView::update, Qt::UniqueConnection);
        }
    }

    deviceTicker.insert(device->serialNumber, n + 1);

    double x = timestamp_to_double(device->timestamp);

    // qDebug() << "timestamp: " << device->timestamp << " x: " << x;

    if (!graph->visible()) {
        // first add a break in the curve by adding a NaN
        double xPrev = graph->data()->at(graph->data()->size() - 1)->key;
        graph->addData((x + xPrev) / 2.0, qQNaN());
        // resume graph visibility
        graph->setVisible(true);
        graph->addToLegend();
    }

    const Quantity &qty = channel->calibratedQuantity;
    double y = quantity_value_as_double(&qty);

    addData(graph, x, y);

    refreshMemoryEstimate();
    replot();
}

void GraphView::removeChannel(const QString &id) {
    QCPGraph *graph = graphByChannelId.value(id);

    if (!graph) {
        return;
    }

    if (!graph->visible()) {
        return;
    }

    TChannel *channel = TDeviceManager::instance().getChannel(id);
    if (!channel) {
        return;
    }

    TDevice *device = channel->device;

    graph->setVisible(false);
    graph->removeFromLegend();

    int n = deviceTicker.value(device->serialNumber) - 1;
    deviceTicker.insert(device->serialNumber, n);

    if (n == 0) {
        // we no longer have any channels from this device
        TDeviceSignal *signal = TDeviceManager::instance().getDeviceSignal(device->serialNumber);
        if (signal) {
            signal->disconnect(this);
        }
    }

    refreshMemoryEstimate();
    replot();
}

void GraphView::update(const TDevice *device) {
    if (device->event == TDevice::UPDATE && graphIntervalMultiple > 1) {
        int64_t dt = device->timestamp - origin;
        int ms = dt / 1000;
        if (ms % graphInterval != 0) {
            return;
        }
    }

    int plotted = 0;

    const int n = device->channels.size();

    for (int i = 0; i < n; i++) {

        TChannel *channel = device->channels[i];
        QCPGraph *graph = graphByChannelId.value(channel->id);

        if (!graph) {
            continue;
        }

        if (!graph->visible()) {
            continue;
        }

        double x = timestamp_to_double(device->timestamp);
        double y = quantity_value_as_double(&channel->calibratedQuantity);

        addData(graph, x, y);

        plotted++;
    }

    if (plotted > 0) {
        replot();
    }
}

void GraphView::addData(QCPGraph *graph, double x, double y) {
    graph->addData(x, y);

    if (x > xMax) {
        xMax = x;
        if (x > xBoundMax) {
            // slide window
            double boundWidth = xBoundMax - xBoundMin;
            xBoundMax = x;
            xBoundMin = x - boundWidth;
        }
    } else if (x < xMin) {
        xMin = x;
    }
}

void GraphView::onThrash(int level) {
    int maxLevel = 1000 / globalInterval;

    if (level > maxLevel) {
        level = (maxLevel > 0) ? maxLevel : 1;
    }

    setIntervalMultiple(graphIntervalMultiple + level);
    graphIntervalWarningLabel->setVisible(true);
    graphIntervalWarningButton->setVisible(true);
}

void GraphView::setIntervalMultiple(int m) {
    if (m <= 0) {
        m = 1;
    }

    graphIntervalMultiple = m;
    graphInterval = m * globalInterval;

    double s = ((double)graphInterval) / 1000.0;
    graphIntervalSpinBox->setValue(s);

    QSettings settings;
    settings.setValue("graph/sample_interval_multiple", m);

    refreshMemoryEstimate();
}

void GraphView::onChannelAliasUpdated(const QString id, const QString alias) {
    QCPGraph *graph = graphByChannelId.value(id);

    if (!graph) {
        return;
    }

    const QString &name = alias.isEmpty() ? id : alias;
    graph->setName(name);
}

void GraphView::onAutoScaleChanged(bool ignored) {
    Qt::Orientations orient = 0;

    if (!xAutoScaleCheckBox->isChecked()) {
        orient |= Qt::Horizontal;
    }

    if (!yAutoScaleCheckBox->isChecked()) {
        orient |= Qt::Vertical;
    }

    plot->axisRect()->setRangeDrag(orient);
    plot->axisRect()->setRangeZoom(orient);

    replot();
}

void GraphView::onYLogScaleChanged(bool ignored) {
    if (yLogScaleCheckBox->isChecked()) {
        plot->yAxis->setScaleType(QCPAxis::stLogarithmic);
        plot->yAxis->setTicker(QSharedPointer<QCPAxisTickerLog>(new QCPAxisTickerLog()));
    } else {
        plot->yAxis->setScaleType(QCPAxis::stLinear);
        plot->yAxis->setTicker(QSharedPointer<QCPAxisTicker>(new QCPAxisTicker()));
    }
}

void GraphView::onGraphQualityChanged() {
    switch (graphQualityPref->getValue()) {

    case GraphQualityPreference::LOW:
        plot->setPlottingHint(QCP::phFastPolylines, true);
        break;

    case GraphQualityPreference::HIGH:
        plot->setPlottingHint(QCP::phFastPolylines, false);
        break;
    }

    replot();
}

void GraphView::onGraphLegendChanged() {
    Qt::AlignmentFlag style = graphLegendPref->getStyle();

    if (style == 0) {
        plot->legend->setVisible(false);
    } else {
        plot->legend->setVisible(true);
        plot->axisRect()->insetLayout()->setInsetAlignment(0, style);
    }

    replot();
}

void GraphView::onBufferSizeSelected(int value) {
    if (value > 1) {
        bufferSpinBox->setSuffix(" days");
    } else {
        bufferSpinBox->setSuffix(" day");
    }

    bufferSpinBoxTimer->start();
}

void GraphView::applyBufferSize() {
    QSettings settings;
    int days = bufferSpinBox->value();
    settings.setValue("graph/buffer_capacity_days", days);

    refreshBounds(days);
    refreshMemoryEstimate();
    replot();
}

void GraphView::onGraphIntervalSelected(double value) {
    hideIntervalWarning();
    graphIntervalSpinBoxTimer->start();
}

void GraphView::applyGraphInterval() {
    double s = graphIntervalSpinBox->value();
    double ms = s * 1000.0;
    graphIntervalMultiple = ((int)round(ms)) / globalInterval;
    setIntervalMultiple(graphIntervalMultiple);
}

void GraphView::hideIntervalWarning() {
    graphIntervalWarningLabel->setVisible(false);
    graphIntervalWarningButton->setVisible(false);
}

void GraphView::refreshBounds(int days) {
    double w1 = xBoundMax - xBoundMin;

    int seconds = days * (24 * 60 * 60);
    double w2 = (double)seconds;

    if (w1 < w2) {
        // grow
        xBoundMax = xBoundMin + w2;
    } else if (w1 > w2) {
        // shrink
        double width = xMax - xMin;
        if (width > w2) {
            // data doesn't fit completely within the new bounds; trim the front
            double delta = width - w2;
            xBoundMin += delta;
        }
        xBoundMax = xBoundMin + w2;
    } else {
        // equal; nothing to do
        return;
    }
}

void GraphView::setGlobalInterval(int ms) {
    globalInterval = ms;

    if (graphInterval > 0) {
        graphIntervalMultiple = graphInterval / ms;
        if (graphIntervalMultiple == 0) {
            graphIntervalMultiple = 1;
        }
        QSettings settings;
        settings.setValue("graph/sample_interval_multiple", graphIntervalMultiple);
    }

    graphInterval = globalInterval * graphIntervalMultiple;

    double global_s = ((double)ms) / 1000.0;
    double graph_s = ((double)graphInterval) / 1000.0;

    graphIntervalSpinBox->setMinimum(global_s);
    graphIntervalSpinBox->setSingleStep(global_s);
    graphIntervalSpinBox->setRange(global_s, global_s * 0x1p14);
    graphIntervalSpinBox->setValue(graph_s);

    refreshMemoryEstimate();
}

void GraphView::refreshMemoryEstimate() {
    int64_t days = bufferSpinBox->value();
    int64_t ms = days * (24 * 60 * 60 * 1000);
    int64_t width = (ms / graphInterval) + 1;      // number of marks along the X axis
    int64_t height = graphByChannelId.size();      // number of marks along the Y axis
    int64_t points = width * height;               // number of points in the graph
    int64_t bytes = points * (2 * sizeof(double)); // two doubles per point

    QString value;

    if (bytes >= (1 << 30)) {
        double size = ((double)bytes) / 0x1p30;
        value.sprintf("%.2f GB", size);
    } else if (bytes >= (1 << 20)) {
        double size = ((double)bytes) / 0x1p20;
        value.sprintf("%.2f MB", size);
    } else if (bytes >= (1 << 10)) {
        double size = ((double)bytes) / 0x1p10;
        value.sprintf("%.2f kB", size);
    } else {
        value.sprintf("%ld B", bytes);
    }

    memoryEstimateLabel->setText(value);
}

void GraphView::replot() {
    if (isPaused) {
        return;
    }

    // Remove data from all graphs, if out of bounds

    if (xMin < xBoundMin) {
        QMapIterator<QString, QCPGraph *> it(graphByChannelId);
        while (it.hasNext()) {
            it.next();
            QCPGraph *graph = it.value();
            graph->data()->removeBefore(xBoundMin - 0.000001);
        }
        xMin = xBoundMin;
    }

    if (xMax > xBoundMax) {
        QMapIterator<QString, QCPGraph *> it(graphByChannelId);
        while (it.hasNext()) {
            it.next();
            QCPGraph *graph = it.value();
            graph->data()->removeAfter(xBoundMax + 0.000001);
        }
        xMax = xBoundMax;
    }

    // Auto scale

    if (xAutoScaleCheckBox->isChecked()) {
        plot->xAxis->rescale(true);
    }

    if (yAutoScaleCheckBox->isChecked()) {
        plot->yAxis->rescale(true);
    }

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

void GraphView::pauseUnpause(void) {
    isPaused = !isPaused;

    if (isPaused) {
        PauseResumeButton->setText(tr("Resume"));
    } else {
        PauseResumeButton->setText(tr("Pause"));
        replot();
    }
}
