//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Data/IntensityDataItem.cpp
//! @brief     Implements class IntensityDataItem
//!
//! @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/Model/Data/IntensityDataItem.h"
#include "Base/Axis/Scale.h"
#include "Base/Util/Assert.h"
#include "Device/Coord/ICoordSystem.h"
#include "Device/Data/Datafield.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Axis/BasicAxisItem.h"
#include "GUI/Model/Data/DataItemUtil.h"
#include "GUI/Model/Data/MaskUnitsConverter.h"
#include "GUI/Model/Data/ProjectionItems.h"
#include "GUI/Support/Util/CoordName.h"
#include "GUI/Support/Util/QCP_Util.h"
#include <qcustomplot.h>

namespace {

namespace Tag {

const QString BaseData("BaseData");
const QString ZAxis("ZAxis");
const QString Interpolation("Interpolation");
const QString Gradient("Gradient");
const QString MaskContainer("MaskContainer");
const QString ProjectionContainer("ProjectionContainer");

} // namespace Tag

QCPColorGradient infernoGradient = GUI::QCP_Util::colorGradientInferno();

// gradient map for colormaps
const QMap<QString, QCPColorGradient::GradientPreset> gradient_map = {
    {"Grayscale", QCPColorGradient::gpGrayscale},
    {"Hot", QCPColorGradient::gpHot},
    {"Cold", QCPColorGradient::gpCold},
    {"Night", QCPColorGradient::gpNight},
    {"Candy", QCPColorGradient::gpCandy},
    {"Geography", QCPColorGradient::gpGeography},
    {"Ion", QCPColorGradient::gpIon},
    {"Thermal", QCPColorGradient::gpThermal},
    {"Polar", QCPColorGradient::gpPolar},
    {"Spectrum", QCPColorGradient::gpSpectrum},
    {"Jet", QCPColorGradient::gpJet},
    {"Hues", QCPColorGradient::gpHues}};

const QMap<QString, QCPColorGradient> custom_gradient_map = {{"Inferno", infernoGradient}};

QString startGradient = "Inferno";

} // namespace

IntensityDataItem::IntensityDataItem()
    : DataItem(M_TYPE)
    , m_isInterpolated(true)
    , m_gradient(
          ComboProperty::fromList(gradient_map.keys() + custom_gradient_map.keys(), startGradient))
    , m_zAxis(std::make_unique<AmplitudeAxisItem>())
{
}

IntensityDataItem::~IntensityDataItem() = default;

void IntensityDataItem::setDatafield(Datafield* data)
{
    if (data != nullptr) {
        ASSERT(data->rank() == 2);
        DataItem::setDatafield(data);
        updateAxesZoomLevel();
        updateAxesLabels();
        updateDataRange();
    } else
        DataItem::setDatafield(data);
}

double IntensityDataItem::xMin() const
{
    const double defaultXmin(0.0);
    return m_datafield ? m_datafield->axis(0).min() : defaultXmin;
}

double IntensityDataItem::xMax() const
{
    const double defaultXmax(1.0);
    return m_datafield ? m_datafield->axis(0).max() : defaultXmax;
}

double IntensityDataItem::yMin() const
{
    const double defaultYmin(0.0);
    return m_datafield ? m_datafield->axis(1).min() : defaultYmin;
}

double IntensityDataItem::yMax() const
{
    const double defaultYmax(1.0);
    return m_datafield ? m_datafield->axis(1).max() : defaultYmax;
}

double IntensityDataItem::lowerZ() const
{
    return zAxisItem()->min();
}

double IntensityDataItem::upperZ() const
{
    return zAxisItem()->max();
}

void IntensityDataItem::setLowerZ(double zmin)
{
    zAxisItem()->setMin(zmin);
    emit itemAxesRangeChanged();
}

void IntensityDataItem::setUpperZ(double zmax)
{
    zAxisItem()->setMax(zmax);
    emit itemAxesRangeChanged();
}

void IntensityDataItem::setLowerAndUpperZ(double zmin, double zmax)
{
    if (lowerZ() != zmin)
        setLowerZ(zmin);

    if (upperZ() != zmax)
        setUpperZ(zmax);
}

void IntensityDataItem::copyZRangeFromItem(DataItem* sourceItem)
{
    IntensityDataItem* source = dynamic_cast<IntensityDataItem*>(sourceItem);
    if (!source || source == this)
        return;
    setLowerZ(source->lowerZ());
    setUpperZ(source->upperZ());
}

QCPColorGradient IntensityDataItem::currentGradientQCP() const
{
    if (gradient_map.contains(currentGradient()))
        return gradient_map.value(currentGradient());
    else
        return custom_gradient_map.value(currentGradient());
}

QString IntensityDataItem::currentGradient() const
{
    return gradientCombo().currentValue();
}

void IntensityDataItem::setCurrentGradient(const QString& gradient)
{
    m_gradient.setCurrentValue(gradient);
    emit gradientChanged();
}

ComboProperty IntensityDataItem::gradientCombo() const
{
    return m_gradient;
}

bool IntensityDataItem::isLog() const
{
    return zAxisItem()->isLogScale();
}

void IntensityDataItem::setLog(bool islog)
{
    zAxisItem()->setLogScale(islog);
}

bool IntensityDataItem::isInterpolated() const
{
    return m_isInterpolated;
}

void IntensityDataItem::setInterpolated(bool interp)
{
    m_isInterpolated = interp;
    emit interpolationChanged(interp);
}

bool IntensityDataItem::isZaxisLocked() const
{
    return m_zAxis->isLocked();
}

void IntensityDataItem::setZaxisLocked(bool state)
{
    return m_zAxis->setLocked(state);
}

void IntensityDataItem::updateCoords(const ICoordSystem& converter)
{
    MaskUnitsConverter mask_converter;
    mask_converter.convertToNbins(this);

    GUI::Model::DataItemUtil::updateDataAxes(this, converter);

    mask_converter.convertFromNbins(this);
}

std::vector<int> IntensityDataItem::shape() const
{
    return {xSize(), ySize()};
}

//! Sets zoom range of X,Y axes, if it was not yet defined.

void IntensityDataItem::updateAxesZoomLevel()
{
    // set zoom range of x-axis to min, max values if it was not set already
    if (upperX() < lowerX()) {
        setLowerX(xMin());
        setUpperX(xMax());
    }

    // set zoom range of y-axis to min, max values if it was not set already
    if (upperY() < lowerY()) {
        setLowerY(yMin());
        setUpperY(yMax());
    }

    const int nx = static_cast<int>(m_datafield->axis(0).size());
    xAxisItem()->setBinCount(nx);
    const int ny = static_cast<int>(m_datafield->axis(1).size());
    yAxisItem()->setBinCount(ny);
}

//! Init axes labels, if it was not done already.

void IntensityDataItem::updateAxesLabels()
{
    if (XaxisTitle().isEmpty())
        setXaxisTitle(QString::fromStdString(m_datafield->axis(0).axisName()));

    if (YaxisTitle().isEmpty())
        setYaxisTitle(QString::fromStdString(m_datafield->axis(1).axisName()));
}

//! Sets min,max values for z-axis, if axes is not locked, and ranges are not yet set.

void IntensityDataItem::updateDataRange()
{
    if (isZaxisLocked())
        return;

    computeDataRange();
    emit alignRanges();
}

void IntensityDataItem::computeDataRange()
{
    setLowerAndUpperZ(dataRange().first, dataRange().second);
}

//! Init zmin, zmax to match the intensity values range.
QPair<double, double> IntensityDataItem::dataRange() const
{
    const Datafield* data = c_field();

    const auto vec = data->flatVector();
    double min(*std::min_element(vec.cbegin(), vec.cend()));
    double max(*std::max_element(vec.cbegin(), vec.cend()));
    if (isLog()) {
        if (max > 10000) {
            min = 1.0;
            max = max * 1.1;
        } else {
            min = max / 10000;
            max = max * 1.1;
        }
    } else {
        max = max * 1.1;
    }

    return QPair<double, double>(min, max);
}

const AmplitudeAxisItem* IntensityDataItem::zAxisItem() const
{
    return m_zAxis.get();
}

AmplitudeAxisItem* IntensityDataItem::zAxisItem()
{
    return m_zAxis.get();
}

void IntensityDataItem::resetView()
{
    if (!m_datafield)
        return;

    setAxesRangeToData();
    if (!isZaxisLocked())
        computeDataRange();
}

void IntensityDataItem::writeTo(QXmlStreamWriter* w) const
{
    XML::writeAttribute(w, XML::Attrib::version, uint(1));

    // parameters from base class
    w->writeStartElement(Tag::BaseData);
    DataItem::writeTo(w);
    w->writeEndElement();

    // interpolation
    w->writeStartElement(Tag::Interpolation);
    XML::writeAttribute(w, XML::Attrib::value, m_isInterpolated);
    w->writeEndElement();

    // gradient
    w->writeStartElement(Tag::Gradient);
    m_gradient.writeTo(w);
    w->writeEndElement();

    // z axis
    w->writeStartElement(Tag::ZAxis);
    m_zAxis->writeTo(w);
    w->writeEndElement();

    // masks
    if (m_maskContainerItem) {
        w->writeStartElement(Tag::MaskContainer);
        m_maskContainerItem->writeTo(w);
        w->writeEndElement();
    }

    // projections
    if (m_projectionContainerItem) {
        w->writeStartElement(Tag::ProjectionContainer);
        m_projectionContainerItem->writeTo(w);
        w->writeEndElement();
    }
}

void IntensityDataItem::readFrom(QXmlStreamReader* r)
{
    const uint version = XML::readUIntAttribute(r, XML::Attrib::version);
    Q_UNUSED(version)

    while (r->readNextStartElement()) {
        QString tag = r->name().toString();

        // parameters from base class
        if (tag == Tag::BaseData) {
            DataItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // interpolation
        } else if (tag == Tag::Interpolation) {
            XML::readAttribute(r, XML::Attrib::value, &m_isInterpolated);
            XML::gotoEndElementOfTag(r, tag);

            // gradient
        } else if (tag == Tag::Gradient) {
            m_gradient.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // z axis
        } else if (tag == Tag::ZAxis) {
            m_zAxis->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // masks
        } else if (tag == Tag::MaskContainer) {
            getOrCreateMaskContainerItem()->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // projections
        } else if (tag == Tag::ProjectionContainer) {
            getOrCreateProjectionContainerItem()->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

MaskContainerItem* IntensityDataItem::maskContainerItem()
{
    return m_maskContainerItem.get();
}

const MaskContainerItem* IntensityDataItem::maskContainerItem() const
{
    return m_maskContainerItem.get();
}

MaskContainerItem* IntensityDataItem::getOrCreateMaskContainerItem()
{
    if (!m_maskContainerItem)
        m_maskContainerItem.reset(new MaskContainerItem);

    return m_maskContainerItem.get();
}

ProjectionContainerItem* IntensityDataItem::projectionContainerItem()
{
    return m_projectionContainerItem.get();
}

const ProjectionContainerItem* IntensityDataItem::projectionContainerItem() const
{
    return m_projectionContainerItem.get();
}

ProjectionContainerItem* IntensityDataItem::getOrCreateProjectionContainerItem()
{
    if (!m_projectionContainerItem)
        m_projectionContainerItem.reset(new ProjectionContainerItem);

    return m_projectionContainerItem.get();
}

bool IntensityDataItem::hasMasks() const
{
    return (maskContainerItem() && !maskContainerItem()->isEmpty());
}

bool IntensityDataItem::hasProjections() const
{
    return (projectionContainerItem() && !projectionContainerItem()->isEmpty());
}
