//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Beam/SourceItems.cpp
//! @brief     Implements BeamItem hierarchy
//!
//! @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/Beam/SourceItems.h"
#include "Base/Axis/Scale.h"
#include "Base/Const/Units.h"
#include "Base/Util/Assert.h"
#include "Device/Beam/Beam.h"
#include "Device/Beam/FootprintGauss.h"
#include "Device/Beam/FootprintSquare.h"
#include "GUI/Model/Axis/PointwiseAxisItem.h"
#include "GUI/Model/Beam/BeamAngleItems.h"
#include "GUI/Model/Beam/BeamWavelengthItem.h"
#include "GUI/Model/Beam/FootprintItems.h"
#include "GUI/Model/Beam/GrazingScanItem.h"
#include "Sim/Scan/IBeamScan.h"

namespace {

namespace Tag {

const QString Intensity("Intensity");
const QString Wavelength("Wavelength");
const QString AzimuthalAngle("AzimuthalAngle");
const QString InclinationAngle("InclinationAngle");
const QString GrazingScan("GrazingScan");
const QString Footprint("Footprint");
const QString BaseData("BaseData");
const QString ExpandBeamParametersGroupbox("ExpandBeamParametersGroupbox");
const QString ExpandFootprintGroupbox("ExpandFootprintGroupbox");

} // namespace Tag

} // namespace

//  ************************************************************************************************
//  SourceItem
//  ************************************************************************************************

SourceItem::SourceItem()
{
    m_intensity.init("Intensity", "Beam intensity in neutrons/photons per sec.", 1e8,
                     Unit::unitless, 3 /* decimals */, RealLimits::limited(0.0, 1e32), "intensity");

    m_azimuthalAngleItem.reset(new BeamAzimuthalAngleItem());
    m_footprint.init("Type", "Footprint type");
}

void SourceItem::writeTo(QXmlStreamWriter* w) const
{
    ASSERT(m_wavelengthItem);

    XML::writeAttribute(w, XML::Attrib::version, uint(2));

    // intensity
    w->writeStartElement(Tag::Intensity);
    m_intensity.writeTo(w);
    w->writeEndElement();

    // wavelength
    w->writeStartElement(Tag::Wavelength);
    m_wavelengthItem->writeTo(w);
    w->writeEndElement();

    // azimuthal angle
    w->writeStartElement(Tag::AzimuthalAngle);
    m_azimuthalAngleItem->writeTo(w);
    w->writeEndElement();

    // beam parameters groupbox: is expanded?
    w->writeStartElement(Tag::ExpandBeamParametersGroupbox);
    XML::writeAttribute(w, XML::Attrib::value, m_expandBeamParameters);
    w->writeEndElement();

    // footprint
    w->writeStartElement(Tag::Footprint);
    m_footprint.writeTo(w);
    w->writeEndElement();

    // footprint groupbox: is expanded?
    w->writeStartElement(Tag::ExpandFootprintGroupbox);
    XML::writeAttribute(w, XML::Attrib::value, m_expandFootprint);
    w->writeEndElement();
}

void SourceItem::readFrom(QXmlStreamReader* r)
{
    ASSERT(m_wavelengthItem);

    const uint version = XML::readUIntAttribute(r, XML::Attrib::version);
    Q_UNUSED(version)

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

        // intensity
        if (tag == Tag::Intensity) {
            m_intensity.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // wavelength
        } else if (tag == Tag::Wavelength) {
            m_wavelengthItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // azimuthal angle
        } else if (tag == Tag::AzimuthalAngle) {
            m_azimuthalAngleItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // beam parameters groupbox: is expanded?
        } else if (tag == Tag::ExpandBeamParametersGroupbox) {
            XML::readAttribute(r, XML::Attrib::value, &m_expandBeamParameters);
            XML::gotoEndElementOfTag(r, tag);

            // footprint
        } else if (tag == Tag::Footprint) {
            m_footprint.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // footprint groupbox: is expanded?
        } else if (tag == Tag::ExpandFootprintGroupbox) {
            XML::readAttribute(r, XML::Attrib::value, &m_expandFootprint);
            XML::gotoEndElementOfTag(r, tag);
        } else
            r->skipCurrentElement();
    }
}

double SourceItem::wavelength() const
{
    ASSERT(m_wavelengthItem);
    return m_wavelengthItem->wavelength();
}

void SourceItem::setWavelength(double value)
{
    ASSERT(m_wavelengthItem);
    m_wavelengthItem->resetToValue(value);
}

BeamWavelengthItem* SourceItem::wavelengthItem() const
{
    ASSERT(m_wavelengthItem);
    return m_wavelengthItem.get();
}

double SourceItem::azimuthalAngle() const
{
    ASSERT(m_azimuthalAngleItem);
    return m_azimuthalAngleItem->azimuthalAngle();
}

void SourceItem::setAzimuthalAngle(double value)
{
    ASSERT(m_azimuthalAngleItem);
    m_azimuthalAngleItem->resetToValue(value);
}

BeamAzimuthalAngleItem* SourceItem::azimuthalAngleItem() const
{
    ASSERT(m_azimuthalAngleItem);
    return m_azimuthalAngleItem.get();
}


void SourceItem::setGaussianFootprint(double value)
{
    m_footprint.setCurrentItem(new FootprintGaussianItem(value));
}

void SourceItem::setSquareFootprint(double value)
{
    m_footprint.setCurrentItem(new FootprintSquareItem(value));
}

void SourceItem::setFootprint(const IFootprint* footprint)
{
    if (!footprint)
        return;

    if (const auto* const fp = dynamic_cast<const FootprintGauss*>(footprint))
        setGaussianFootprint(fp->widthRatio());
    else if (const auto* const fp = dynamic_cast<const FootprintSquare*>(footprint))
        setSquareFootprint(fp->widthRatio());
}

//  ************************************************************************************************
//  BeamItem
//  ************************************************************************************************

BeamItem::BeamItem()
{
    m_wavelengthItem.reset(new BeamWavelengthItem);
    m_inclinationAngleItem.reset(new BeamInclinationAngleItem);
}

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

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

    // inclination angle
    w->writeStartElement(Tag::InclinationAngle);
    m_inclinationAngleItem->writeTo(w);
    w->writeEndElement();
}

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

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

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

            // inclination angle
        } else if (tag == Tag::InclinationAngle) {
            m_inclinationAngleItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

void BeamItem::setInclinationAngle(double value)
{
    ASSERT(m_inclinationAngleItem);
    m_inclinationAngleItem->resetToValue(value);
}

BeamDistributionItem* BeamItem::beamDistributionItem() const
{
    ASSERT(m_inclinationAngleItem);
    return m_inclinationAngleItem.get();
}

double BeamItem::getInclinationAngle() const
{
    return dynamic_cast<BeamInclinationAngleItem*>(beamDistributionItem())->inclinationAngle();
}

std::unique_ptr<Beam> BeamItem::createBeam() const
{
    double lambda = wavelength();
    double inclination_angle = Units::deg2rad(getInclinationAngle());
    double azimuthal_angle = Units::deg2rad(azimuthalAngle());

    auto result =
        std::make_unique<Beam>(Beam(intensity(), lambda, inclination_angle, azimuthal_angle));
    result->setFootprint(m_footprint.currentItem()->createFootprint().get());
    return result;
}

//  ************************************************************************************************
//  ScanItem
//  ************************************************************************************************

ScanItem::ScanItem()
{
    m_grazingScanItem.reset(new GrazingScanItem());
    m_wavelengthItem.reset(new BeamWavelengthItem(true));
}

void ScanItem::setScan(const IBeamScan* scan)
{
    setIntensity(scan->intensity());
    setWavelength(scan->wavelength());
    setAzimuthalAngle(0.0);

    auto* axis_item = inclinationAxisItem();
    const Scale* axis = scan->coordinateAxis();
    ASSERT(axis);
    // TODO do we need to check that axis is equidistant?
    axis_item->setBinCount(static_cast<int>(axis->size()));
    axis_item->setMin(axis->min() / Units::deg);
    axis_item->setMax(axis->max() / Units::deg);
    axis_item->setTitle(QString::fromStdString(axis->axisName()));

    setFootprint(scan->footprint());
}

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

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

    // grazing scan
    w->writeStartElement(Tag::GrazingScan);
    m_grazingScanItem->writeTo(w);
    w->writeEndElement();
}

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

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

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

            // footprint (moved to base class since v21 (version == 2))
        } else if (version == 1 && tag == Tag::Footprint) {
            m_footprint.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // footprint groupbox: is expanded? (moved to base class since v21 (version == 2))
        } else if (version == 1 && tag == Tag::ExpandFootprintGroupbox) {
            XML::readAttribute(r, XML::Attrib::value, &m_expandFootprint);
            XML::gotoEndElementOfTag(r, tag);

            // grazing scan
        } else if (tag == Tag::GrazingScan) {
            m_grazingScanItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

double ScanItem::getInclinationAngle() const
{
    return 0.0;
}

GrazingScanItem* ScanItem::grazingScanItem() const
{
    return m_grazingScanItem.get();
}

BasicAxisItem* ScanItem::inclinationAxisItem() const
{
    return grazingScanItem()->alphaAxisItem();
}

void ScanItem::updateToData(const Scale& axis, QString units, const ICoordSystem& converter)
{
    if (units == "nbins") {
        grazingScanItem()->initUniformAxis(axis);
        grazingScanItem()->selectUniformAxis();
    } else {
        grazingScanItem()->initListScan(axis, units, converter);
        grazingScanItem()->selectListScan();
    }
}

std::unique_ptr<Beam> ScanItem::createBeam() const
{
    double lambda = wavelength();
    double inclination_angle = Units::deg2rad(getInclinationAngle());
    double azimuthal_angle = Units::deg2rad(azimuthalAngle());

    return std::make_unique<Beam>(Beam(intensity(), lambda, inclination_angle, azimuthal_angle));
}
