//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Descriptor/DistributionItems.cpp
//! @brief     Implements class DistributionItem and several subclasses.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2022
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/Model/Descriptor/DistributionItems.h"
#include "GUI/Model/Util/UtilXML.h"
#include "Param/Distrib/Distributions.h"

namespace {
namespace Tag {

const QString NumberOfSamples("NumberOfSamples");
const QString RelSamplingWidth("RelSamplingWidth");
const QString Mean("Mean");
const QString Minimum("Minimum");
const QString Maximum("Maximum");
const QString HWHM("HWHM");
const QString StandardDeviation("StandardDeviation");
const QString Median("Median");
const QString ScaleParameter("ScaleParameter");
const QString Sigma("Sigma");
const QString Center("Center");
const QString LeftWidth("LeftWidth");
const QString MiddleWidth("MiddleWidth");
const QString RightWidth("RightWidth");
const QString BaseData("BaseData");

} // namespace Tag
} // namespace

DistributionItem::DistributionItem() = default;

void DistributionItem::initRelSamplingWidth()
{
    m_rel_sampling_width.init("Rel. sampling width", "", 2.0, "relSamplingWidth");
}

bool DistributionItem::hasRelSamplingWidth() const
{
    return m_rel_sampling_width.isInitialized();
}

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

    // number of samples
    w->writeStartElement(Tag::NumberOfSamples);
    XML::writeAttribute(w, XML::Attrib::value, m_number_of_samples);
    w->writeEndElement();

    // relative sampling width
    if (hasRelSamplingWidth()) {
        w->writeStartElement(Tag::RelSamplingWidth);
        m_rel_sampling_width.writeTo(w);
        w->writeEndElement();
    }
}

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

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

        // number of samples
        if (tag == Tag::NumberOfSamples) {
            XML::readAttribute(r, XML::Attrib::value, &m_number_of_samples);
            XML::gotoEndElementOfTag(r, tag);

            // relative sampling width
        } else if (tag == Tag::RelSamplingWidth) {
            m_rel_sampling_width.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

// --------------------------------------------------------------------------------------------- //

SymmetricResolutionItem::SymmetricResolutionItem(double mean, int decimals,
                                                 const QString& meanLabel)
{
    m_mean.init(meanLabel, "", mean, decimals, RealLimits::limitless(), "mean");
}

void SymmetricResolutionItem::setMeanDecimals(uint d)
{
    m_mean.setDecimals(d);
}

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

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

    // mean
    w->writeStartElement(Tag::Mean);
    m_mean.writeTo(w);
    w->writeEndElement();
}

void SymmetricResolutionItem::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) {
            DistributionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // mean
        } else if (tag == Tag::Mean) {
            m_mean.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

// --------------------------------------------------------------------------------------------- //

DistributionNoneItem::DistributionNoneItem()
    : SymmetricResolutionItem(0.1, 3, "Value")
{
}

std::unique_ptr<IDistribution1D> DistributionNoneItem::createDistribution(double) const
{
    return {};
}

double DistributionNoneItem::deviation(double) const
{
    return 0.0;
}

void DistributionNoneItem::initDistribution(double value)
{
    setMean(value);
}

DoubleProperties DistributionNoneItem::distributionValues(bool withMean)
{
    return withMean ? DoubleProperties{&m_mean} : DoubleProperties{};
}

// --------------------------------------------------------------------------------------------- //

DistributionGateItem::DistributionGateItem()
{
    m_minimum.init("Min", "", 0.0, 3, RealLimits::limitless(), "min");
    m_maximum.init("Max", "", 1.0, 3, RealLimits::limitless(), "max");
}

std::unique_ptr<IDistribution1D> DistributionGateItem::createDistribution(double scale) const
{
    return std::make_unique<DistributionGate>(scale * m_minimum.value(), scale * m_maximum.value(),
                                              m_number_of_samples);
}

void DistributionGateItem::initDistribution(double value)
{
    double sigma(0.1 * std::abs(value));
    if (sigma == 0.0)
        sigma = 0.1;
    m_minimum.setValue(value - sigma);
    m_maximum.setValue(value + sigma);
}

void DistributionGateItem::setRange(double min, double max)
{
    m_minimum.setValue(min);
    m_maximum.setValue(max);
}

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

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

    // minimum
    w->writeStartElement(Tag::Minimum);
    m_minimum.writeTo(w);
    w->writeEndElement();

    // maximum
    w->writeStartElement(Tag::Maximum);
    m_maximum.writeTo(w);
    w->writeEndElement();
}

void DistributionGateItem::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) {
            DistributionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // minimum
        } else if (tag == Tag::Minimum) {
            m_minimum.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // maximum
        } else if (tag == Tag::Maximum) {
            m_maximum.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

DoubleProperties DistributionGateItem::distributionValues(bool /*withMean*/)
{
    return {&m_minimum, &m_maximum};
}

// --------------------------------------------------------------------------------------------- //

DistributionLorentzItem::DistributionLorentzItem()
    : SymmetricResolutionItem(1.0)
{
    initRelSamplingWidth();

    m_hwhm.init("HWHM", "", 1.0, "hwhm");
}

std::unique_ptr<IDistribution1D> DistributionLorentzItem::createDistribution(double scale) const
{
    return std::make_unique<DistributionLorentz>(scale * m_mean.value(), scale * m_hwhm.value(),
                                                 m_number_of_samples, m_rel_sampling_width);
}

double DistributionLorentzItem::deviation(double scale) const
{
    return m_hwhm * scale;
}

void DistributionLorentzItem::initDistribution(double value)
{
    double hw(0.1 * std::abs(value));
    if (hw == 0.0)
        hw = 0.1;

    setMean(value);
    setHwhm(hw);
}

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

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

    // half width at half maximum
    w->writeStartElement(Tag::HWHM);
    m_hwhm.writeTo(w);
    w->writeEndElement();
}

void DistributionLorentzItem::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) {
            SymmetricResolutionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // half width at half maximum
        } else if (tag == Tag::HWHM) {
            m_hwhm.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

DoubleProperties DistributionLorentzItem::distributionValues(bool withMean)
{
    return withMean ? DoubleProperties{&m_mean, &m_hwhm, &m_rel_sampling_width}
                    : DoubleProperties{&m_hwhm, &m_rel_sampling_width};
}

// --------------------------------------------------------------------------------------------- //

DistributionGaussianItem::DistributionGaussianItem()
    : SymmetricResolutionItem(1.0)
{
    initRelSamplingWidth();

    m_standard_deviation.init("StdDev", "", 1.0, 3, RealLimits::lowerLimited(0.0), "stdDev");
}

std::unique_ptr<IDistribution1D> DistributionGaussianItem::createDistribution(double scale) const
{
    return std::make_unique<DistributionGaussian>(scale * m_mean.value(),
                                                  scale * m_standard_deviation.value(),
                                                  m_number_of_samples, m_rel_sampling_width);
}

double DistributionGaussianItem::deviation(double scale) const
{
    return m_standard_deviation.value() * scale;
}

void DistributionGaussianItem::initDistribution(double value)
{
    double stddev(0.1 * std::abs(value));
    if (stddev == 0.0)
        stddev = 0.1;

    setMean(value);
    setStandardDeviation(stddev);
}

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

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

    // standard deviation
    w->writeStartElement(Tag::StandardDeviation);
    m_standard_deviation.writeTo(w);
    w->writeEndElement();
}

void DistributionGaussianItem::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) {
            SymmetricResolutionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // standard deviation
        } else if (tag == Tag::StandardDeviation) {
            m_standard_deviation.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

DoubleProperties DistributionGaussianItem::distributionValues(bool withMean)
{
    return withMean ? DoubleProperties{&m_mean, &m_standard_deviation, &m_rel_sampling_width}
                    : DoubleProperties{&m_standard_deviation, &m_rel_sampling_width};
}

// --------------------------------------------------------------------------------------------- //

DistributionLogNormalItem::DistributionLogNormalItem()
{
    initRelSamplingWidth();

    m_median.init("Median", "", 1.0, "median");
    m_scale_parameter.init("ScaleParameter", "", 1.0, 3, RealLimits::lowerLimited(0.0), "scalePar");
}

std::unique_ptr<IDistribution1D> DistributionLogNormalItem::createDistribution(double scale) const
{
    return std::make_unique<DistributionLogNormal>(scale * m_median.value(),
                                                   m_scale_parameter.value(), m_number_of_samples,
                                                   m_rel_sampling_width);
}

void DistributionLogNormalItem::initDistribution(double value)
{
    double scale(0.1 * std::abs(value));
    if (scale == 0.0)
        scale = 0.1;

    setMedian(value);
    setScaleParameter(scale);
}

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

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

    // median
    w->writeStartElement(Tag::Median);
    m_median.writeTo(w);
    w->writeEndElement();

    // scale parameter
    w->writeStartElement(Tag::ScaleParameter);
    m_scale_parameter.writeTo(w);
    w->writeEndElement();
}

void DistributionLogNormalItem::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) {
            DistributionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // median
        } else if (tag == Tag::Median) {
            m_median.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // scale parameter
        } else if (tag == Tag::ScaleParameter) {
            m_scale_parameter.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

DoubleProperties DistributionLogNormalItem::distributionValues(bool /*withMean*/)
{
    return {&m_median, &m_scale_parameter, &m_rel_sampling_width};
}

// --------------------------------------------------------------------------------------------- //

DistributionCosineItem::DistributionCosineItem()
    : SymmetricResolutionItem(1.0)
{
    m_hwhm.init("HWHM", "", 1.0, 3, RealLimits::lowerLimited(0.0), "hwhm");
}

std::unique_ptr<IDistribution1D> DistributionCosineItem::createDistribution(double scale) const
{
    return std::make_unique<DistributionCosine>(scale * m_mean.value(), scale * m_hwhm.value(),
                                                m_number_of_samples);
}

double DistributionCosineItem::deviation(double scale) const
{
    return m_hwhm.value() * scale;
}

void DistributionCosineItem::initDistribution(double value)
{
    double sigma(0.1 * std::abs(value));
    if (sigma == 0.0)
        sigma = 0.1;

    setMean(value);
    setHwhm(sigma);
}

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

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

    // sigma
    w->writeStartElement(Tag::Sigma);
    m_hwhm.writeTo(w);
    w->writeEndElement();
}

void DistributionCosineItem::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) {
            SymmetricResolutionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // sigma
        } else if (tag == Tag::Sigma) {
            m_hwhm.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

DoubleProperties DistributionCosineItem::distributionValues(bool withMean)
{
    return withMean ? DoubleProperties{&m_mean, &m_hwhm} : DoubleProperties{&m_hwhm};
}

// --------------------------------------------------------------------------------------------- //

DistributionTrapezoidItem::DistributionTrapezoidItem()
{
    m_center.init("Center", "", 1.0, 3, RealLimits::limitless(), "center");
    m_left_width.init("LeftWidth", "", 1.0, "left");
    m_middle_width.init("MiddleWidth", "", 1.0, "middle");
    m_right_width.init("RightWidth", "", 1.0, "right");
}

std::unique_ptr<IDistribution1D> DistributionTrapezoidItem::createDistribution(double scale) const
{
    return std::make_unique<DistributionTrapezoid>(
        scale * m_center.value(), scale * m_left_width.value(), scale * m_middle_width.value(),
        scale * m_right_width.value(), m_number_of_samples);
}

void DistributionTrapezoidItem::initDistribution(double value)
{
    double width(0.1 * std::abs(value));
    if (width == 0.0)
        width = 0.1;
    setCenter(value);
    setLeftWidth(width);
    setMiddleWidth(width);
    setRightWidth(width);
}

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

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

    // center
    w->writeStartElement(Tag::Center);
    m_center.writeTo(w);
    w->writeEndElement();

    // left width
    w->writeStartElement(Tag::LeftWidth);
    m_left_width.writeTo(w);
    w->writeEndElement();

    // middle width
    w->writeStartElement(Tag::MiddleWidth);
    m_middle_width.writeTo(w);
    w->writeEndElement();

    // right width
    w->writeStartElement(Tag::RightWidth);
    m_right_width.writeTo(w);
    w->writeEndElement();
}

void DistributionTrapezoidItem::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) {
            DistributionItem::readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // center
        } else if (tag == Tag::Center) {
            m_center.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // left width
        } else if (tag == Tag::LeftWidth) {
            m_left_width.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // middle width
        } else if (tag == Tag::MiddleWidth) {
            m_middle_width.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // right width
        } else if (tag == Tag::RightWidth) {
            m_right_width.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

DoubleProperties DistributionTrapezoidItem::distributionValues(bool withMean)
{
    return withMean ? DoubleProperties{&m_center, &m_left_width, &m_middle_width, &m_right_width}
                    : DoubleProperties{&m_left_width, &m_middle_width, &m_right_width};
}
