//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Plotter/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/Plotter/SpecularPlot.h"
#include "Base/Axis/Frame.h"
#include "Base/Py/PyFmt.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/Data1DItem.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Base/Fontsize.h"
#include "GUI/View/Plotter/RangeUtil.h"

namespace {

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

} // namespace

SpecularPlot::SpecularPlot()
    : ScientificPlot(PLOT_TYPE::Plot1D)
{
    auto* vlayout = new QVBoxLayout(this);
    vlayout->setContentsMargins(0, 0, 0, 0);
    vlayout->setSpacing(0);
    vlayout->addWidget(m_qcp);
    setLayout(vlayout);

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

    setMouseTrackingEnabled(true);
}

void SpecularPlot::setData1DItems(const QVector<Data1DItem*>& items)
{
    m_data_items = items;

    // set nullptr at destruction
    for (auto*& item : m_data_items)
        if (item) {
            disconnect(item, &DataItem::destroyed, this, nullptr);
            connect(item, &DataItem::destroyed, [&item] { item = nullptr; });
        }

    initPlot();
    connectItems();
}

QVector<Data1DItem*> SpecularPlot::data_items() const
{
    QVector<Data1DItem*> nonNullItems;
    for (auto* item : m_data_items)
        if (item)
            nonNullItems.append(item);
    return nonNullItems;
}

Data1DItem* SpecularPlot::currentData1DItem() const
{
    if (data_items().empty())
        return nullptr;

    return data_items().first();
}

QString SpecularPlot::infoString(const QPointF& pos) const
{
    double x = m_qcp->xAxis->pixelToCoord(pos.x());
    double y = m_qcp->yAxis->pixelToCoord(pos.y());

    int ix = bin(x, m_qcp->graph());

    QString intensityString = QString::fromStdString(Py::Fmt::printScientificDouble(y));

    return QString(" [x: %1, y: %2]    [binx: %3]")
        .arg(QString::number(x, 'f', 4))
        .arg(intensityString)
        .arg(ix, 2);
}

void SpecularPlot::setLog()
{
    if (!currentData1DItem())
        return;
    GUI::QCP_RangeUtil::setLogz(m_qcp->yAxis, currentData1DItem()->isLog());
    GUI::QCP_RangeUtil::setLogz(m_qcp->yAxis2, currentData1DItem()->isLog());
    replot();
}

void SpecularPlot::onXaxisRangeChanged(QCPRange range) const
{
    for (auto* item : data_items())
        item->setXrange(range.lower, range.upper);
    gDoc->setModified();
    if (currentData1DItem())
        emit currentData1DItem()->updateOtherPlots(currentData1DItem());
}

void SpecularPlot::onYaxisRangeChanged(QCPRange range) const
{
    for (auto* item : data_items())
        item->setYrange(range.lower, range.upper);
    gDoc->setModified();
    if (currentData1DItem())
        emit currentData1DItem()->updateOtherPlots(currentData1DItem());
}

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

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

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

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

    setConnected(true);
}

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

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

    m_qcp->xAxis->setTickLabelFont(QFont(QFont().family(), GUI::Style::fontSizeSmall()));
    m_qcp->yAxis->setTickLabelFont(QFont(QFont().family(), GUI::Style::fontSizeSmall()));

    setPlot();
}

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

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

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

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

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

void SpecularPlot::setAxesLabels()
{
    if (!currentData1DItem())
        return;
    m_qcp->xAxis->setLabel(currentData1DItem()->xAxisLabel());
    m_qcp->yAxis->setLabel("Reflectivity");
    replot();
}

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

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

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

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

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
