//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/PlotSpecular/SpecularPlot.cpp
//! @brief     Implements class SpecularPlot
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/View/PlotSpecular/SpecularPlot.h"
#include "Base/Axis/Frame.h"
#include "Base/Util/Assert.h"
#include "Device/Data/Datafield.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Axis/BasicAxisItem.h"
#include "GUI/Model/Data/SpecularDataItem.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/Support/Util/QCP_Util.h"
#include "GUI/View/PlotUtil/PlotConstants.h"
#include "GUI/View/PlotUtil/PlotEventInfo.h"
#include "GUI/View/Tool/UpdateTimer.h"

namespace {

const int replot_update_interval = 10;

int bin(double x, const QCPGraph* graph);

} // namespace

SpecularPlot::SpecularPlot(QWidget* parent)
    : ScientificPlot(parent, PLOT_TYPE::Plot1D)
    , m_custom_plot(new QCustomPlot)
    , m_update_timer(new UpdateTimer(replot_update_interval, this))
{
    auto* vlayout = new QVBoxLayout(this);
    vlayout->setContentsMargins(0, 0, 0, 0);
    vlayout->setSpacing(0);
    vlayout->addWidget(m_custom_plot);
    setLayout(vlayout);

    m_custom_plot->setAttribute(Qt::WA_NoMousePropagation, false);

    setMouseTrackingEnabled(true);
}

void SpecularPlot::setSpecularItems(const QList<SpecularDataItem*>& dataItems)
{
    ScientificPlot::setSpecularItems(dataItems);
    initPlot();
    connectItems();
}

PlotEventInfo SpecularPlot::eventInfo(double xpos, double ypos) const
{
    PlotEventInfo result(plotType());

    result.setX(xpos);
    result.setValue(ypos);

    result.setInAxesRange(axesRangeContains(xpos, ypos));
    result.setNx(bin(result.x(), m_custom_plot->graph()));

    return result;
}

void SpecularPlot::setLog()
{
    if (!currentSpecularDataItem())
        return;
    GUI::QCP_Util::setLogz(m_custom_plot->yAxis, currentSpecularDataItem()->isLog());
    GUI::QCP_Util::setLogz(m_custom_plot->yAxis2, currentSpecularDataItem()->isLog());
    replot();
}

void SpecularPlot::onXaxisRangeChanged(QCPRange newRange)
{
    for (auto item : specularDataItems()) {
        item->setLowerX(newRange.lower);
        item->setUpperX(newRange.upper);
    }
    gProjectDocument.value()->setModified();
    if (currentSpecularDataItem())
        emit currentSpecularDataItem()->updateOtherPlots(currentSpecularDataItem());
}

void SpecularPlot::onYaxisRangeChanged(QCPRange newRange)
{
    for (auto item : specularDataItems()) {
        item->setLowerY(newRange.lower);
        item->setUpperY(newRange.upper);
    }
    gProjectDocument.value()->setModified();
    if (currentSpecularDataItem())
        emit currentSpecularDataItem()->updateOtherPlots(currentSpecularDataItem());
}

void SpecularPlot::onTimeToReplot()
{
    m_custom_plot->replot();
}

void SpecularPlot::connectItems()
{
    // data
    for (auto item : specularDataItems())
        connect(item, &DataItem::datafieldChanged, this, &SpecularPlot::initPlot,
                Qt::UniqueConnection);

    // units
    for (auto item : specularDataItems())
        connect(item, &DataItem::axesUnitsReplotRequested, this, &SpecularPlot::setPlot,
                Qt::UniqueConnection);

    // x axis
    connect(currentSpecularDataItem()->xAxisItem(), &BasicAxisItem::axisTitleChanged, this,
            &SpecularPlot::setAxesLabels, Qt::UniqueConnection);
    connect(currentSpecularDataItem()->xAxisItem(), &BasicAxisItem::axisRangeChanged, this,
            &SpecularPlot::setAxesRanges, Qt::UniqueConnection);

    // y axis
    connect(currentSpecularDataItem()->yAxisItem(), &BasicAxisItem::axisTitleChanged, this,
            &SpecularPlot::setAxesLabels, Qt::UniqueConnection);
    connect(currentSpecularDataItem()->yAxisItem(), &BasicAxisItem::axisRangeChanged, this,
            &SpecularPlot::setAxesRanges, Qt::UniqueConnection);
    connect(currentSpecularDataItem()->yAxisItem(), &AmplitudeAxisItem::logScaleChanged, this,
            &SpecularPlot::setLog, Qt::UniqueConnection);

    setConnected(true);
}

void SpecularPlot::initPlot()
{
    m_custom_plot->clearPlottables(); // clear graphs and error bars
    m_graph_map.clear();
    m_errorbar_map.clear();
    for (auto item : specularDataItems()) {
        m_custom_plot->addGraph();
        m_graph_map.insert(item, m_custom_plot->graph());
        m_custom_plot->graph()->setLineStyle(item->lineStyle());
        m_custom_plot->graph()->setPen(QPen(item->color(), item->thickness()));
        m_custom_plot->graph()->setScatterStyle(
            QCPScatterStyle(item->scatter(), item->scatterSize()));

        // create error bars
        const Datafield* data = item->c_field();
        if (data && data->hasErrorSigmas()) {
            QCPErrorBars* errorBars = new QCPErrorBars(m_custom_plot->xAxis, m_custom_plot->yAxis);
            m_errorbar_map.insert(item, errorBars);
            errorBars->removeFromLegend();
            errorBars->setAntialiased(false);
            errorBars->setDataPlottable(m_custom_plot->graph());
            errorBars->setPen(QPen(QColor(180, 180, 180)));
        }
    }

    m_custom_plot->xAxis->setTickLabelFont(
        QFont(QFont().family(), GUI::Constants::plot_tick_label_size()));
    m_custom_plot->yAxis->setTickLabelFont(
        QFont(QFont().family(), GUI::Constants::plot_tick_label_size()));

    setPlot();
}

void SpecularPlot::setConnected(bool isConnected)
{
    setAxesRangeConnected(isConnected);
    setUpdateTimerConnected(isConnected);
}

void SpecularPlot::setAxesRangeConnected(bool isConnected)
{
    if (isConnected) {
        connect(m_custom_plot->xAxis,
                static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged), this,
                &SpecularPlot::onXaxisRangeChanged, Qt::UniqueConnection);
        connect(m_custom_plot->yAxis,
                static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged), this,
                &SpecularPlot::onYaxisRangeChanged, Qt::UniqueConnection);
    } else {
        disconnect(m_custom_plot->xAxis,
                   static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged), this,
                   &SpecularPlot::onXaxisRangeChanged);
        disconnect(m_custom_plot->yAxis,
                   static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged), this,
                   &SpecularPlot::onYaxisRangeChanged);
    }
}

void SpecularPlot::setUpdateTimerConnected(bool isConnected)
{
    if (isConnected)
        connect(m_update_timer, &UpdateTimer::timeToUpdate, this, &SpecularPlot::onTimeToReplot,
                Qt::UniqueConnection);
    else
        disconnect(m_update_timer, &UpdateTimer::timeToUpdate, this, &SpecularPlot::onTimeToReplot);
}

void SpecularPlot::setPlot()
{
    for (auto item : specularDataItems())
        setDataFromItem(item);
    setAxes();
    setAxesLabels();
    replot();
}

void SpecularPlot::setAxes()
{
    m_custom_plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
    m_custom_plot->axisRect()->setupFullAxesBox(true);
    setAxesRanges();
    setLog();
}

void SpecularPlot::setAxesRanges()
{
    if (!currentSpecularDataItem())
        return;
    setAxesRangeConnected(false);
    m_custom_plot->xAxis->setRange(currentSpecularDataItem()->lowerX(),
                                   currentSpecularDataItem()->upperX());
    m_custom_plot->yAxis->setRange(currentSpecularDataItem()->lowerY(),
                                   currentSpecularDataItem()->upperY());
    setAxesRangeConnected(true);
    replot();
}

void SpecularPlot::setAxesLabels()
{
    if (!currentSpecularDataItem())
        return;
    setLabel(currentSpecularDataItem()->xAxisItem(), m_custom_plot->xAxis);
    setLabel(currentSpecularDataItem()->yAxisItem(), m_custom_plot->yAxis);
    replot();
}

void SpecularPlot::setLabel(const BasicAxisItem* item, QCPAxis* axis)
{
    ASSERT(item && axis);
    axis->setLabel(item->title());
}

void SpecularPlot::setDataFromItem(SpecularDataItem* item)
{
    ASSERT(item && m_graph_map.contains(item));
    QCPGraph* graph = m_graph_map.value(item);
    graph->data()->clear();

    const Datafield* data = item->c_field();
    if (!data)
        return;

    for (size_t i = 0; i < data->size(); ++i) {
        double x = data->frame().projectedCoord(i, 0);
        double y = data->operator[](i);
        graph->addData(x, y);
    }

    // set error bars
    if (data->hasErrorSigmas()) {
        ASSERT(m_errorbar_map.contains(item));
        QCPErrorBars* errorBars = m_errorbar_map.value(item);
        for (size_t i = 0; i < data->size(); ++i) {
            double y_Err = data->errorSigmas()[i];
            errorBars->addData(y_Err);
        }
    }
}

void SpecularPlot::replot()
{
    m_update_timer->scheduleUpdate();
}

namespace {

int bin(double x, const QCPGraph* graph)
{
    const int key_start = graph->findBegin(x);
    const int key_end = graph->findBegin(x, false); // false = do not expand range
    if (key_end == key_start || key_end == graph->dataCount())
        return key_start;
    return (x - graph->dataSortKey(key_start)) <= (graph->dataSortKey(key_end) - x) ? key_start
                                                                                    : key_end;
}

} // namespace
