//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Device/DistributionPlot.cpp
//! @brief     Implements class DistributionPlot.
//!
//! @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/Device/DistributionPlot.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Beam/DistributionItems.h"
#include "GUI/View/Info/CautionSign.h"
#include "Param/Distrib/Distributions.h"
#include "Param/Distrib/ParameterSample.h"

DistributionPlot::DistributionPlot(QWidget* parent)
    : QWidget(parent)
    , m_qcp(new QCustomPlot)
    , m_dist_item(nullptr)
    , m_label(new QLabel)
    , m_reset_action(new QAction(this))
    , m_caution_sign(new CautionSign(this))
{
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

    m_reset_action->setText("Reset View");
    connect(m_reset_action, &QAction::triggered, this, &DistributionPlot::resetView);

    m_label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);

    auto* mainLayout = new QVBoxLayout;
    mainLayout->addWidget(m_qcp, 1);
    mainLayout->addWidget(m_label);
    m_qcp->setAttribute(Qt::WA_NoMousePropagation, false);
    setLayout(mainLayout);

    connect(m_qcp, &QCustomPlot::mousePress, this, &DistributionPlot::onMousePress);
    connect(m_qcp, &QCustomPlot::mouseMove, this, &DistributionPlot::onMouseMove);
}

void DistributionPlot::setDistItem(DistributionItem* distItem)
{
    ASSERT(distItem);
    if (m_dist_item == distItem)
        return;

    m_dist_item = distItem;
    plotItem();
}

void DistributionPlot::plotItem()
{
    init_plot();

    if (!m_dist_item->is<DistributionNoneItem>()) {
        try {
            plot_distributions();
        } catch (const std::exception& ex) {
            init_plot();
            QString message = QString("Wrong parameters\n\n") + (QString::fromStdString(ex.what()));
            m_caution_sign->setCautionMessage(message);
        }
    }

    m_qcp->replot();
}

//! Generates label with current mouse position.

void DistributionPlot::onMouseMove(QMouseEvent* event)
{
    QPoint point = event->pos();
    double xPos = m_qcp->xAxis->pixelToCoord(point.x());
    double yPos = m_qcp->yAxis->pixelToCoord(point.y());

    if (m_qcp->xAxis->range().contains(xPos) && m_qcp->yAxis->range().contains(yPos)) {
        QString text = QString("[x:%1, y:%2]").arg(xPos).arg(yPos);
        m_label->setText(text);
    }
}

void DistributionPlot::onMousePress(QMouseEvent* event)
{
    if (event->button() == Qt::RightButton) {
        QPoint point = event->globalPos();
        QMenu menu;
        menu.addAction(m_reset_action);
        menu.exec(point);
    }
}

//! Reset zoom range to initial state.

void DistributionPlot::resetView()
{
    m_qcp->xAxis->setRange(m_x_range);
    m_qcp->yAxis->setRange({0, 1.1});
    m_qcp->replot();
}

//! Clears all plottables, resets axes to initial state.

void DistributionPlot::init_plot()
{
    m_caution_sign->clear();

    m_qcp->clearGraphs();
    m_qcp->clearItems();
    m_qcp->clearPlottables();
    m_qcp->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectAxes | QCP::iSelectLegend
                           | QCP::iSelectPlottables);
    m_qcp->yAxis->setLabel("probability");
    m_qcp->yAxis->setTickLabels(false);
    m_qcp->xAxis2->setVisible(true);
    m_qcp->yAxis2->setVisible(true);
    m_qcp->xAxis2->setTickLabels(false);
    m_qcp->yAxis2->setTickLabels(false);
    m_qcp->xAxis2->setTicks(false);
    m_qcp->yAxis2->setTicks(false);

    m_qcp->yAxis->setRange({0, 1.1});
    setPlotRange({-1, 1});
}

void DistributionPlot::plot_distributions()
{
    ASSERT(!m_dist_item->is<DistributionNoneItem>());

    auto distrib = m_dist_item->createDistribution();

    //... Plot function graph
    std::vector<std::pair<double, double>> graph = distrib->plotGraph();
    double max_y = 0;
    for (auto& i : graph)
        max_y = std::max(max_y, i.second);

    QVector<double> xFunc(graph.size());
    QVector<double> yFunc(graph.size());
    for (size_t i = 0; i < graph.size(); ++i) {
        xFunc[i] = graph[i].first;
        yFunc[i] = graph[i].second / max_y;
    }

    setPlotRange({xFunc.first(), xFunc.last()});

    m_qcp->addGraph();
    m_qcp->graph(0)->setData(xFunc, yFunc);

    //... Plot bars to represent weighted sampling points
    std::vector<ParameterSample> samples = distrib->distributionSamples();
    size_t N = samples.size();
    max_y = 0;
    for (size_t i = 0; i < N; ++i)
        max_y = std::max(max_y, samples[i].weight);

    QVector<double> xBar(N);
    QVector<double> yBar(N);
    for (size_t i = 0; i < N; ++i) {
        xBar[i] = samples[i].value;
        yBar[i] = samples[i].weight / max_y;
    }

    // use rational function to set bar width:
    // - at low N, a constant fraction of the plot range
    // - at large N, decreasing with N^-1
    double barWidth = (xFunc.last() - xFunc.first()) / (30 + 3 * N * N / (10 + N));

    auto* bars = new QCPBars(m_qcp->xAxis, m_qcp->yAxis);
    bars->setWidth(barWidth);
    bars->setData(xBar, yBar);
}

void DistributionPlot::setPlotRange(const std::pair<double, double>& xRange)
{
    m_x_range = QCPRange(xRange.first, xRange.second);
    m_qcp->xAxis->setRange(m_x_range);
}

void DistributionPlot::plotVerticalLine(double xMin, double yMin, double xMax, double yMax,
                                        const QColor& color)
{
    auto* line = new QCPItemLine(m_qcp);

    QPen pen(color, 1, Qt::DashLine);
    line->setPen(pen);
    line->setSelectable(true);

    line->start->setCoords(xMin, yMin);
    line->end->setCoords(xMax, yMax);
}

void DistributionPlot::setXAxisName(const QString& xAxisName)
{
    m_qcp->xAxis->setLabel(xAxisName);
}

void DistributionPlot::setShowMouseCoords(bool b)
{
    m_label->setVisible(b);
}
