//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Plotter/ProjectionsPlot.cpp
//! @brief     Implements class ProjectionsPlot.
//!
//! @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/ProjectionsPlot.h"
#include "Base/Axis/Scale.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/Data2DItem.h"
#include "GUI/Model/Mask/MasksSet.h" // projections
#include "GUI/View/Base/Fontsize.h"
#include "GUI/View/Plotter/RangeUtil.h"
#include <qcustomplot.h>
#include <utility>

ProjectionsPlot::ProjectionsPlot(Qt::Orientation orientation, QWidget* parent)
    : QWidget(parent)
    , m_orientation(orientation)
    , m_qcp(new QCustomPlot)
{
    auto* vlayout = new QVBoxLayout(this);
    vlayout->setContentsMargins(0, 0, 0, 0);
    vlayout->setSpacing(0);
    vlayout->addWidget(m_qcp);
    m_qcp->setAttribute(Qt::WA_NoMousePropagation, false);

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

    QCPAxisRect* axisRectangle = m_qcp->axisRect();
    axisRectangle->setMinimumSize(0, 160);
    axisRectangle->setAutoMargins(QCP::msTop | QCP::msBottom);
}

void ProjectionsPlot::setData2DItem(Data2DItem* item)
{
    m_data_item = item;

    // set nullptr at destruction
    if (m_data_item) {
        disconnect(m_data_item, &DataItem::destroyed, this, nullptr);
        connect(m_data_item, &DataItem::destroyed, [this] { m_data_item = nullptr; });
    }

    clearAll();
    connectItems();
}

void ProjectionsPlot::onMarginsChanged(double left, double right)
{
    const QSize base_size = GUI::Style::SizeOfLetterM(m_qcp);
    const int top = static_cast<int>(base_size.height() * 1.5);
    const int bottom = static_cast<int>(base_size.height() * 4.5);
    m_qcp->axisRect()->setMargins(QMargins(left, top, right, bottom));
    replot();
}

void ProjectionsPlot::connectItems()
{
    if (!m_data_item)
        return;

    // Units changed
    connect(m_data_item, &Data2DItem::axesUnitsReplotRequested, this,
            &ProjectionsPlot::updateProjections, Qt::UniqueConnection);

    // Update projection plot on new item appearance
    connect(m_data_item, &Data2DItem::projectionCreated, this, &ProjectionsPlot::updateProjections,
            Qt::UniqueConnection);

    // Update projection position
    connect(m_data_item, &Data2DItem::projectionPositionChanged, this,
            &ProjectionsPlot::onProjectionPropertyChanged, Qt::UniqueConnection);

    // Remove projection plot
    connect(m_data_item, &Data2DItem::projectionGone, this, &ProjectionsPlot::clearProjection,
            Qt::UniqueConnection);

    // Values of intensity changed, regenerate everything.
    connect(m_data_item, &Data2DItem::datafieldChanged, this,
            &ProjectionsPlot::updateProjectionsData, Qt::UniqueConnection);

    // interpolation changed
    connect(m_data_item, &Data2DItem::interpolationChanged, this, &ProjectionsPlot::setInterpolate,
            Qt::UniqueConnection);

    // AXES
    // if the colormap is zoomed or dragged:
    connect(m_data_item, &Data2DItem::updateOtherPlots, this, &ProjectionsPlot::updateAxesRange,
            Qt::UniqueConnection);

    // if axes are changed externally, from the properties panel:
    // axes range
    connect(m_data_item->axItemX(), &BasicAxisItem::axisRangeChanged, this,
            &ProjectionsPlot::updateAxesRange, Qt::UniqueConnection);
    connect(m_data_item->axItemY(), &BasicAxisItem::axisRangeChanged, this,
            &ProjectionsPlot::updateAxesRange, Qt::UniqueConnection);
    connect(m_data_item->zAxisItem(), &BasicAxisItem::axisRangeChanged, this,
            &ProjectionsPlot::updateAxesRange, Qt::UniqueConnection);

    // axes title
    connect(m_data_item->axItemX(), &BasicAxisItem::axisTitleChanged, this,
            &ProjectionsPlot::updateAxesTitle, Qt::UniqueConnection);
    connect(m_data_item->axItemY(), &BasicAxisItem::axisTitleChanged, this,
            &ProjectionsPlot::updateAxesTitle, Qt::UniqueConnection);

    // z log scale
    connect(m_data_item->zAxisItem(), &AmplitudeAxisItem::logScaleChanged, this,
            &ProjectionsPlot::setLogz, Qt::UniqueConnection);

    updateProjectionsData();
}

void ProjectionsPlot::disconnectItems()
{
    if (!m_data_item)
        return;

    disconnect(m_data_item, nullptr, this, nullptr);
    disconnect(m_data_item->axItemX(), nullptr, this, nullptr);
    disconnect(m_data_item->axItemY(), nullptr, this, nullptr);
    disconnect(m_data_item->zAxisItem(), nullptr, this, nullptr);
}

QCPGraph* ProjectionsPlot::graphForItem(const LineItem* item)
{
    if (!m_data_item)
        return nullptr;

    QCPGraph* graph = m_item_to_graph[item];
    if (!graph) {
        graph = m_qcp->addGraph();
        QPen pen;
        pen.setColor(QColor(0, 0, 255, 200));
        graph->setLineStyle(m_data_item->isInterpolated() ? QCPGraph::lsLine
                                                          : QCPGraph::lsStepCenter);
        graph->setPen(pen);
        m_item_to_graph[item] = graph;
    }

    return graph;
}

//! Creates cached 2D histogram for later projection calculations.

void ProjectionsPlot::updateProjectionsData()
{
    if (!m_data_item)
        return;
    updateAxesRange();
    updateAxesTitle();
    setLogz(m_data_item->isLog());
    updateProjections();
}

//! Runs through all projection items and generates missed plots.

void ProjectionsPlot::updateProjections()
{
    ASSERT(m_data_item);
    auto* all_prjns = m_data_item->prjns();
    if (!all_prjns)
        return;
    for (const auto* prjn : *all_prjns)
        if (const auto* t = dynamic_cast<const LineItem*>(prjn))
            if (t->orientation() == m_orientation)
                setGraphFromItem(t);
    replot();
}

void ProjectionsPlot::onProjectionPropertyChanged(const LineItem* item)
{
    if (!item)
        return;
    setGraphFromItem(item);
    replot();
}

//! Updates canvas axes to match current zoom level of Data2DItem

void ProjectionsPlot::updateAxesRange()
{
    if (!m_data_item)
        return;

    if (m_orientation == Qt::Horizontal)
        m_qcp->xAxis->setRange(QCPRange(m_data_item->lowerX(), m_data_item->upperX()));
    else
        m_qcp->xAxis->setRange(QCPRange(m_data_item->lowerY(), m_data_item->upperY()));

    m_qcp->yAxis->setRange(QCPRange(m_data_item->lowerZ(), m_data_item->upperZ()));
}

void ProjectionsPlot::updateAxesTitle()
{
    if (!m_data_item)
        return;

    if (m_orientation == Qt::Horizontal)
        m_qcp->xAxis->setLabel(m_data_item->xAxisLabel());
    else
        m_qcp->xAxis->setLabel(m_data_item->yAxisLabel());
}

//! Removes plot corresponding to given projection item.

void ProjectionsPlot::clearProjection(const LineItem* item)
{
    if (QCPGraph* graph = graphForItem(item)) {
        m_qcp->removePlottable(graph);
        m_item_to_graph.remove(item);
        replot();
    }
}

void ProjectionsPlot::clearAll()
{
    m_qcp->clearPlottables();
    m_item_to_graph.clear();
    replot();
}

//! Sets the data to graph from given projection item. When all graphs are set, call replot.

void ProjectionsPlot::setGraphFromItem(const LineItem* item)
{
    if (!m_data_item || !m_data_item->c_field())
        return;

    QCPGraph* graph = graphForItem(item);
    if (!graph)
        return;

    // TODO: merge with similar code in ProjectionsSaver::projectionsData ?
    std::unique_ptr<Datafield> field;
    if (dynamic_cast<const HorizontalLineItem*>(item)) {
        if (m_orientation != Qt::Horizontal)
            return;
        field.reset(m_data_item->c_field()->xProjection(item->pos()));
    } else if (dynamic_cast<const VerticalLineItem*>(item)) {
        if (m_orientation != Qt::Vertical)
            return;
        field.reset(m_data_item->c_field()->yProjection(item->pos()));
    } else
        ASSERT_NEVER;

    auto centers = field->axis(0).binCenters();
    auto values = field->flatVector();
    graph->setData(QVector<double>(centers.begin(), centers.end()),
                   QVector<double>(values.begin(), values.end()));
}

void ProjectionsPlot::setInterpolate(bool isInterpolated)
{
    for (auto* graph : m_item_to_graph)
        graph->setLineStyle(isInterpolated ? QCPGraph::lsLine : QCPGraph::lsStepCenter);
}

void ProjectionsPlot::setLogz(bool isLogz)
{
    GUI::QCP_RangeUtil::setLogz(m_qcp->yAxis, isLogz);
}

void ProjectionsPlot::replot()
{
    m_qcp->replot();
}
