//  ************************************************************************************************
//
//  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/Data/Data1DItem.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Base/Fontsize.h"
#include "GUI/View/Plotter/RangeUtil.h"

namespace {

int val2bin(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


SpecularPlot::SpecularPlot()
{
    setAttribute(Qt::WA_NoMousePropagation, false);
    setMouseTrackingEnabled(true);
}

void SpecularPlot::onDataDestroyed(QObject* destroyed_obj)
{
    for (auto*& item : m_data_items)
        if (item && item == destroyed_obj)
            item = nullptr;
}

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

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

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

    return data_items().first();
}

QString SpecularPlot::infoString(double x, double y) const
{
    int ix = ::val2bin(x, 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::applyLogXstate()
{
    if (!currentData1DItem())
        return;
    GUI::QCP_RangeUtil::setLogArgumentAxis(xAxis, currentData1DItem()->isLogX());
    GUI::QCP_RangeUtil::setLogArgumentAxis(xAxis2, currentData1DItem()->isLogX());
    replot();
}

void SpecularPlot::applyLogYstate()
{
    if (!currentData1DItem())
        return;
    GUI::QCP_RangeUtil::setLogAmplitudeAxis(yAxis, currentData1DItem()->isLogY());
    GUI::QCP_RangeUtil::setLogAmplitudeAxis(yAxis2, currentData1DItem()->isLogY());
    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()
{
    // set nullptr at destruction
    for (auto* item : data_items())
        connect(item, &QObject::destroyed, this, &SpecularPlot::onDataDestroyed,
                Qt::UniqueConnection);

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

    // x axis
    connect(currentData1DItem()->axItemX(), &BasicAxisItem::axisTitleChanged, this,
            &SpecularPlot::setAxesLabels, Qt::UniqueConnection);
    connect(currentData1DItem()->axItemX(), &BasicAxisItem::axisRangeChanged, this,
            &SpecularPlot::setAxesRanges, Qt::UniqueConnection);
    connect(currentData1DItem()->axItemX(), &BasicAxisItem::logScaleChanged, this,
            &SpecularPlot::applyLogXstate, 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(), &BasicAxisItem::logScaleChanged, this,
            &SpecularPlot::applyLogYstate, Qt::UniqueConnection);

    setConnected(true);
}

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

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

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

    setPlot();
}

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

void SpecularPlot::setAxesRangeConnected(bool isConnected)
{
    if (isConnected) {
        connect(xAxis, static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged),
                this, &SpecularPlot::onXaxisRangeChanged, Qt::UniqueConnection);
        connect(yAxis, static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged),
                this, &SpecularPlot::onYaxisRangeChanged, Qt::UniqueConnection);
    } else {
        disconnect(xAxis, static_cast<void (QCPAxis::*)(const QCPRange&)>(&QCPAxis::rangeChanged),
                   this, &SpecularPlot::onXaxisRangeChanged);
        disconnect(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()
{
    setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
    axisRect()->setupFullAxesBox(true);
    setAxesRanges();
    applyLogXstate();
    applyLogYstate();
}

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

void SpecularPlot::setAxesLabels()
{
    if (!currentData1DItem())
        return;
    xAxis->setLabel(currentData1DItem()->xAxisLabel());
    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);
        }
    }
}
