//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Projection/ProjectionsPlot.cpp
//! @brief     Defines class ProjectionCanvas
//!
//! @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/Projection/ProjectionsPlot.h"
#include "Base/Axis/Scale.h"
#include "Device/Data/Datafield.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Axis/BasicAxisItem.h"
#include "GUI/Model/Data/IntensityDataItem.h"
#include "GUI/Model/Data/MaskItems.h"
#include "GUI/Model/Data/ProjectionItems.h"
#include "GUI/Support/Util/QCP_Util.h"
#include "GUI/View/PlotUtil/PlotConstants.h"
#include "GUI/View/PlotUtil/RangeUtil.h"
#include <boost/polymorphic_cast.hpp>
#include <qcustomplot.h>
#include <utility>

using boost::polymorphic_downcast;

ProjectionsPlot::ProjectionsPlot(GUI::ID::ProjectionType projectionType, QWidget* parent)
    : DataItemBundleWidget(parent)
    , m_projectionType(projectionType)
    , m_customPlot(new QCustomPlot)
{
    auto* vlayout = new QVBoxLayout(this);
    vlayout->setContentsMargins(0, 0, 0, 0);
    vlayout->setSpacing(0);
    vlayout->addWidget(m_customPlot);
    m_customPlot->setAttribute(Qt::WA_NoMousePropagation, false);
    setLayout(vlayout);

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

    GUI::QCP_Util::setDefaultMargins(m_customPlot);
}

void ProjectionsPlot::setIntensityItem(IntensityDataItem* intensityDataItem)
{
    DataItemBundleWidget::setIntensityItem(intensityDataItem);
    clearAll();
    connectItems();
}

void ProjectionsPlot::onMarginsChanged(double left, double right)
{
    QMargins orig = m_customPlot->axisRect()->margins();
    m_customPlot->axisRect()->setMargins(QMargins(left, orig.top(), right, orig.bottom()));
    replot();
}

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

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

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

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

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

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

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

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

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

    // axes title
    connect(intensityItem()->xAxisItem(), &BasicAxisItem::axisTitleChanged, this,
            &ProjectionsPlot::updateAxesTitle, Qt::UniqueConnection);
    connect(intensityItem()->yAxisItem(), &BasicAxisItem::axisTitleChanged, this,
            &ProjectionsPlot::updateAxesTitle, Qt::UniqueConnection);

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

    updateProjectionsData();
}

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

    disconnect(intensityItem(), nullptr, this, nullptr);
    disconnect(intensityItem()->xAxisItem(), nullptr, this, nullptr);
    disconnect(intensityItem()->yAxisItem(), nullptr, this, nullptr);
    disconnect(intensityItem()->zAxisItem(), nullptr, this, nullptr);
}

IntensityDataItem* ProjectionsPlot::intensityItem()
{
    return intensityDataItem();
}

ProjectionContainerItem* ProjectionsPlot::projectionContainerItem()
{
    ProjectionContainerItem* result = intensityItem()->projectionContainerItem();
    ASSERT(result);
    return result;
}

QVector<MaskItem*> ProjectionsPlot::projectionItems()
{
    return projectionContainerItem()->projectionsOfType(m_projectionType);
}

bool ProjectionsPlot::isCorrectProjectionType(MaskItem* item)
{
    if (isHorizontalType() && dynamic_cast<HorizontalLineItem*>(item))
        return true;

    if (!isHorizontalType() && dynamic_cast<VerticalLineItem*>(item))
        return true;

    return false;
}

QCPGraph* ProjectionsPlot::graphForItem(MaskItemObject* item)
{
    if (!intensityItem())
        return nullptr;

    QCPGraph* graph = m_item_to_graph[item];
    if (!graph) {
        graph = m_customPlot->addGraph();
        QPen pen;
        pen.setColor(QColor(0, 0, 255, 200));
        graph->setLineStyle(intensityItem()->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 (!intensityItem())
        return;

    updateAxesRange();
    updateAxesTitle();
    setLogz(intensityItem()->isLog());
    updateProjections();
}

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

void ProjectionsPlot::updateProjections()
{
    for (auto* projItem : projectionItems()) {
        if (isCorrectProjectionType(projItem))
            setGraphFromItem(graphForItem(projItem), projItem);
    }
    replot();
}

void ProjectionsPlot::onProjectionPropertyChanged(MaskItemObject* item)
{
    auto* projection = dynamic_cast<MaskItem*>(item);
    ASSERT(projection);
    if (isCorrectProjectionType(projection)) {
        if (auto* graph = graphForItem(projection))
            setGraphFromItem(graph, projection);
    }
    replot();
}

//! Updates canva's axes to match current zoom level of IntensityDataItem

void ProjectionsPlot::updateAxesRange()
{
    const IntensityDataItem* ii = intensityItem();
    if (!ii)
        return;

    if (isHorizontalType())
        m_customPlot->xAxis->setRange(GUI::View::RangeUtil::itemZoomX(ii));
    else
        m_customPlot->xAxis->setRange(GUI::View::RangeUtil::itemZoomY(ii));

    m_customPlot->yAxis->setRange(GUI::View::RangeUtil::itemDataZoom(ii));
}

void ProjectionsPlot::updateAxesTitle()
{
    const IntensityDataItem* ii = intensityItem();
    if (!ii)
        return;

    if (isHorizontalType())
        m_customPlot->xAxis->setLabel(ii->XaxisTitle());
    else
        m_customPlot->xAxis->setLabel(ii->YaxisTitle());
}

//! Removes plot corresponding to given projection item.

void ProjectionsPlot::clearProjection(MaskItemObject* item)
{
    if (auto* graph = graphForItem(item)) {
        m_customPlot->removePlottable(graph);
        m_item_to_graph.remove(item);
        replot();
    }
}

void ProjectionsPlot::clearAll()
{
    // also removes projections from other intensity maps that are not in projectionItems() list
    m_customPlot->clearPlottables();
    m_item_to_graph.clear();
    replot();
}

//! Sets the data to graph from given projection iten.

void ProjectionsPlot::setGraphFromItem(QCPGraph* graph, MaskItem* item)
{
    const IntensityDataItem* ii = intensityItem();
    if (!ii || !ii->c_field())
        return;

    std::unique_ptr<Datafield> field;

    // TODO: merge with very similar code in SaveProjectionsAssistant::projectionsData
    if (const auto* horLine = dynamic_cast<HorizontalLineItem*>(item))
        field.reset(ii->c_field()->xProjection(horLine->posY()));
    else if (const auto* verLine = dynamic_cast<VerticalLineItem*>(item))
        field.reset(ii->c_field()->yProjection(verLine->posX()));
    else
        ASSERT(false);


    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_Util::setLogz(m_customPlot->yAxis, isLogz);
}

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

//! Returns true, if widget is intended for horizontal projections.

bool ProjectionsPlot::isHorizontalType()
{
    return m_projectionType == GUI::ID::ProjectionType::Horizontal;
}
