//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Models/InstrumentItems.cpp
//! @brief     Implement class InstrumentItem and all its children
//!
//! @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/Models/InstrumentItems.h"
#include "Base/Const/Units.h"
#include "Base/Pixel/RectangularPixel.h"
#include "Core/Simulation/DepthProbeSimulation.h"
#include "Device/Coord/CoordSystem1D.h"
#include "Device/Detector/RectangularDetector.h"
#include "Device/Detector/SphericalDetector.h"
#include "Device/Instrument/CoordSystem2D.h"
#include "Device/Instrument/Instrument.h"
#include "GUI/Models/BackgroundItems.h"
#include "GUI/Models/BeamWavelengthItem.h"
#include "GUI/Models/DataItem.h"
#include "GUI/Models/DetectorItems.h"
#include "GUI/Models/Error.h"
#include "GUI/Models/GroupItem.h"
#include "GUI/Models/ItemFileNameUtils.h"
#include "GUI/Models/JobItemUtils.h"
#include "GUI/Models/MaskItems.h"
#include "GUI/Models/PointwiseAxisItem.h"
#include "GUI/Models/RealDataItem.h"
#include "GUI/Models/SessionModel.h"
#include "GUI/Models/SpecularBeamInclinationItem.h"
#include "GUI/Models/TransformToDomain.h"
#include "GUI/utils/GUIHelpers.h"

namespace {

const QString background_group_label = "Type";
const QStringList instrument_names{"GISASInstrument", "OffSpecularInstrument",
                                   "SpecularInstrument"};

BasicAxisItem* addAxisGroupProperty(SessionItem* parent, const QString& tag)
{
    BasicAxisItem* axisItem = parent->addProperty<BasicAxisItem>(tag);
    axisItem->setToolTip("Incoming alpha range [deg]");
    axisItem->titleItem()->setVisible(false);
    axisItem->binsItem()->setToolTip("Number of points in scan");
    axisItem->lowerBoundItem()->setToolTip("Starting value [deg]");
    axisItem->upperBoundItem()->setToolTip("Ending value [deg]");

    axisItem->setTitle("alpha_i");
    axisItem->setLowerBound(0.0);
    axisItem->setUpperBound(10.0);

    return axisItem;
}

} // namespace


//  ************************************************************************************************
//  class InstrumentItem
//  ************************************************************************************************

const QString InstrumentItem::P_IDENTIFIER = "Identifier";
const QString InstrumentItem::P_BEAM = "Beam";
const QString InstrumentItem::P_BACKGROUND = "Background";

QString InstrumentItem::id() const
{
    return getItemValue(P_IDENTIFIER).toString();
}

void InstrumentItem::setId(const QString& id)
{
    setItemValue(P_IDENTIFIER, id);
}

void InstrumentItem::setName(const QString& instrumentName)
{
    setItemName(instrumentName);
}

QString InstrumentItem::name() const
{
    return itemName();
}

BeamItem* InstrumentItem::beamItem() const
{
    return item<BeamItem>(P_BEAM);
}

BackgroundItem* InstrumentItem::backgroundItem() const
{
    return &groupItem<BackgroundItem>(P_BACKGROUND);
}

GroupItem* InstrumentItem::backgroundGroup()
{
    return item<GroupItem>(P_BACKGROUND);
}

bool InstrumentItem::alignedWith(const RealDataItem* item) const
{
    return shape() == item->shape();
}

std::unique_ptr<Instrument> InstrumentItem::createInstrument() const
{
    std::unique_ptr<Instrument> result(new Instrument);
    result->setBeam(*beamItem()->createBeam());

    return result;
}

InstrumentItem::InstrumentItem(const QString& modelType) : SessionItem(modelType)
{
    setItemName(modelType);
    addProperty(P_IDENTIFIER, GUIHelpers::createUuid())->setVisible(false);
}

void InstrumentItem::initBackgroundGroup()
{
    auto item = addGroupProperty(P_BACKGROUND, "Background group");
    item->setDisplayName(background_group_label);
    item->setToolTip("Background type");
}

template <typename T> void InstrumentItem::addBeam()
{
    addProperty<T>(P_BEAM);
}

template <typename T> T* InstrumentItem::beam() const
{
    return item<T>(P_BEAM);    
}

//  ************************************************************************************************
//  class SpecularInstrumentItem
//  ************************************************************************************************

SpecularInstrumentItem::SpecularInstrumentItem() : InstrumentItem("SpecularInstrument")
{
    addBeam<SpecularBeamItem>();

    initBackgroundGroup();
    beam<SpecularBeamItem>()->updateFileName(
        ItemFileNameUtils::instrumentDataFileName(*this));
}

SpecularBeamItem* SpecularInstrumentItem::beamItem() const
{
    return beam<SpecularBeamItem>();
}

SpecularInstrumentItem::~SpecularInstrumentItem() = default;

std::vector<int> SpecularInstrumentItem::shape() const
{
    const auto axis_item = beamItem()->currentInclinationAxisItem();
    return {axis_item->binCount()};
}

void SpecularInstrumentItem::updateToRealData(const RealDataItem* item)
{
    if (shape().size() != item->shape().size())
        throw Error("Error in SpecularInstrumentItem::updateToRealData: The type "
                    "of instrument is incompatible with passed data shape.");

    const auto& data = item->nativeOutputData()->axis(0);
    beamItem()->updateToData(data, item->nativeDataUnits());
}

bool SpecularInstrumentItem::alignedWith(const RealDataItem* item) const
{
    const QString native_units = item->nativeDataUnits();
    if (native_units == "nbins") {
        return beamItem()->currentInclinationAxisItem()->modelType() == BasicAxisItem::M_TYPE
               && shape() == item->shape();
    } else {
        auto axis_item = dynamic_cast<PointwiseAxisItem*>(beamItem()->currentInclinationAxisItem());
        if (!axis_item)
            return false;
        if (axis_item->getUnitsLabel() != native_units)
            return false;

        auto instrument_axis = axis_item->axis();
        if (!instrument_axis)
            return false;

        if (!item->hasNativeData())
            return false;

        const auto& native_axis = item->nativeOutputData()->axis(0);
        return *instrument_axis == native_axis;
    }
}

ICoordSystem* SpecularInstrumentItem::createCoordSystem() const
{
    const auto instrument = createInstrument();
    auto axis_item = beamItem()->currentInclinationAxisItem();
    if (auto pointwise_axis = dynamic_cast<PointwiseAxisItem*>(axis_item)) {
        if (!pointwise_axis->containsNonXMLData()) // workaround for loading project
            return nullptr;
        Axes::Coords native_units = JobItemUtils::coordsFromName(pointwise_axis->getUnitsLabel());
        return new AngularReflectometryCoordinates(instrument->beam().wavelength(),
                                                   *pointwise_axis->axis(), native_units);
    }

    return new AngularReflectometryCoordinates(instrument->beam().wavelength(),
                                               *axis_item->createAxis(1.0), Axes::Coords::DEGREES);
}

QString SpecularInstrumentItem::defaultName() const
{
    return "Specular";
}


//  ************************************************************************************************
//  class Instrument2DItem
//  ************************************************************************************************

const QString Instrument2DItem::P_DETECTOR = "Detector";

Instrument2DItem::Instrument2DItem(const QString& modelType) : InstrumentItem(modelType)
{
    addBeam<GISASBeamItem>();
    addGroupProperty(P_DETECTOR, "Detector group");
    initBackgroundGroup();

    setDefaultTag(P_DETECTOR);
}

Instrument2DItem::~Instrument2DItem() = default;

DetectorItem* Instrument2DItem::detectorItem() const
{
    return &groupItem<DetectorItem>(P_DETECTOR);
}

GroupItem* Instrument2DItem::detectorGroup()
{
    return item<GroupItem>(P_DETECTOR);
}

void Instrument2DItem::setDetectorGroup(const QString& modelType)
{
    setGroupProperty(P_DETECTOR, modelType);
}

void Instrument2DItem::clearMasks()
{
    detectorItem()->clearMasks();
}

void Instrument2DItem::importMasks(const MaskContainerItem* maskContainer)
{
    detectorItem()->importMasks(maskContainer);
}

std::unique_ptr<Instrument> Instrument2DItem::createInstrument() const
{
    auto result = InstrumentItem::createInstrument();
    result->setDetector(*detectorItem()->createDetector());

    return result;
}


//  ************************************************************************************************
//  class GISASInstrumentItem
//  ************************************************************************************************

GISASInstrumentItem::GISASInstrumentItem() : Instrument2DItem("GISASInstrument") {}

std::vector<int> GISASInstrumentItem::shape() const
{
    auto detector_item = detectorItem();
    return {detector_item->xSize(), detector_item->ySize()};
}

void GISASInstrumentItem::updateToRealData(const RealDataItem* item)
{
    if (!item)
        return;

    const auto data_shape = item->shape();
    if (shape().size() != data_shape.size())
        throw Error("Error in GISASInstrumentItem::updateToRealData: The type of "
                    "instrument is incompatible with passed data shape.");
    detectorItem()->setXSize(data_shape[0]);
    detectorItem()->setYSize(data_shape[1]);
}

QString GISASInstrumentItem::defaultName() const
{
    return "GISAS";
}

ICoordSystem* GISASInstrumentItem::createCoordSystem() const
{
    const auto instrument = createInstrument();
    instrument->initDetector();
    return instrument->createScatteringCoords();
}

//  ************************************************************************************************
//  class OffSpecularInstrumentItem
//  ************************************************************************************************

const QString OffSpecularInstrumentItem::P_ALPHA_AXIS = "Alpha axis";

OffSpecularInstrumentItem::OffSpecularInstrumentItem() : Instrument2DItem("OffSpecularInstrument")
{
    BasicAxisItem* axis_item = addAxisGroupProperty(this, P_ALPHA_AXIS);
    auto inclination_item = axis_item->lowerBoundItem();
    auto beam_item = beamItem();
    beam_item->setInclinationAngle(inclination_item->value().toDouble());
    beam_item->inclinationAngleItem()->setEnabled(false);
    inclination_item->mapper()->setOnValueChange([beam_item, inclination_item]() {
        beam_item->setInclinationAngle(inclination_item->value().toDouble());
    });
}

std::vector<int> OffSpecularInstrumentItem::shape() const
{
    const int x_size = item<BasicAxisItem>(P_ALPHA_AXIS)->binCount();
    auto detector_item = detectorItem();
    return {x_size, detector_item->ySize()};
}

void OffSpecularInstrumentItem::updateToRealData(const RealDataItem* dataItem)
{
    if (!dataItem)
        return;

    const auto data_shape = dataItem->shape();
    if (shape().size() != data_shape.size())
        throw Error("Error in OffSpecularInstrumentItem::updateToRealData: The type of "
                    "instrument is incompatible with passed data shape.");

    item<BasicAxisItem>(P_ALPHA_AXIS)->setBinCount(data_shape[0]);
    detectorItem()->setYSize(data_shape[1]);
}

QString OffSpecularInstrumentItem::defaultName() const
{
    return "OffSpecular";
}

ICoordSystem* OffSpecularInstrumentItem::createCoordSystem() const
{
    const auto instrument = createInstrument();
    instrument->initDetector();
    const auto axis_item = item<BasicAxisItem>(OffSpecularInstrumentItem::P_ALPHA_AXIS);
    const auto detector2d = dynamic_cast<const IDetector2D*>(instrument->getDetector());
    const auto axes = detector2d->axesClippedToRegionOfInterest();
    ASSERT(axes.size() == 2);
    const IAxis& yAxis = *axes[1];
    const Beam& beam = instrument->beam();
    const auto alphaAxis = axis_item->createAxis(Units::deg);

    if (const auto* rectDetector = dynamic_cast<const RectangularDetector*>(detector2d)) {
        std::unique_ptr<RectangularPixel> detectorPixel(rectDetector->regionOfInterestPixel());
        return OffSpecularCoordinates::createForRectangularDetector(beam, *alphaAxis,
                                                                    *detectorPixel, yAxis);
    } else if (dynamic_cast<const SphericalDetector*>(detector2d))
        return OffSpecularCoordinates::createForSphericalDetector(beam, *alphaAxis, yAxis);

    ASSERT(0);
    return nullptr;
}

//  ************************************************************************************************
//  class DepthProbeInstrumentItem
//  ************************************************************************************************


const QString DepthProbeInstrumentItem::P_Z_AXIS = "Z axis";

DepthProbeInstrumentItem::DepthProbeInstrumentItem() : InstrumentItem("DepthProbeInstrument")
{
    setItemName("DepthProbeInstrument");

    addBeam<SpecularBeamItem>();

    auto axisItem = beamItem()->currentInclinationAxisItem();
    axisItem->setLowerBound(0.0);
    axisItem->setUpperBound(1.0);
    axisItem->setBinCount(500);

    auto axis = addProperty<BasicAxisItem>(P_Z_AXIS);
    axis->setLowerBound(-100.0);
    axis->setUpperBound(100.0);
    axis->titleItem()->setVisible(false);
    axis->binsItem()->setToolTip("Number of points in scan across sample bulk");
    axis->lowerBoundItem()->setToolTip("Starting value below sample horizont in nm");
    axis->upperBoundItem()->setToolTip("Ending value above sample horizont in nm");
}

SpecularBeamItem* DepthProbeInstrumentItem::beamItem() const
{
    return beam<SpecularBeamItem>();
}

std::unique_ptr<Instrument> DepthProbeInstrumentItem::createInstrument() const
{
    throw std::runtime_error("DepthProbeInstrumentItem::createInstrument()");
}

std::vector<int> DepthProbeInstrumentItem::shape() const
{
    return std::vector<int>(); // no certain shape to avoid linking to real data
}

void DepthProbeInstrumentItem::updateToRealData(const RealDataItem*)
{
    throw std::runtime_error("DepthProbeInstrumentItem::updateToRealData()");
}

QString DepthProbeInstrumentItem::defaultName() const
{
    return "DepthProbe";
}

std::unique_ptr<DepthProbeSimulation> DepthProbeInstrumentItem::createSimulation() const
{
    std::unique_ptr<DepthProbeSimulation> simulation = std::make_unique<DepthProbeSimulation>();

    const auto axis_item = beamItem()->currentInclinationAxisItem();

    auto axis = axis_item->createAxis(Units::deg);

    simulation->setBeamParameters(beamItem()->wavelength(), static_cast<int>(axis->size()),
                                  axis->lowerBound(), axis->upperBound());

    auto depthAxisItem = dynamic_cast<BasicAxisItem*>(getItem(P_Z_AXIS));
    auto depthAxis = depthAxisItem->createAxis(1.0);
    simulation->setZSpan(depthAxis->size(), depthAxis->lowerBound(), depthAxis->upperBound());

    TransformToDomain::setBeamDistribution(ParameterDistribution::BeamWavelength,
                                           *beamItem()->wavelengthItem(), *simulation.get());

    TransformToDomain::setBeamDistribution(ParameterDistribution::BeamInclinationAngle,
                                           *beamItem()->inclinationAngleItem(), *simulation.get());

    return simulation;
}

ICoordSystem* DepthProbeInstrumentItem::createCoordSystem() const
{
    return createSimulation()->createCoordSystem();
}