//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/FromCore/ItemizeSimulation.cpp
//! @brief     Implements functions that convert ISimulation from core to GUI items
//!
//! @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/FromCore/ItemizeSimulation.h"
#include "Base/Axis/Scale.h"
#include "Base/Const/Units.h"
#include "Device/Beam/Beam.h"
#include "Device/Detector/OffspecDetector.h"
#include "Device/Detector/SphericalDetector.h"
#include "Device/Mask/DetectorMask.h"
#include "Device/Mask/Ellipse.h"
#include "Device/Mask/InfinitePlane.h"
#include "Device/Mask/Line.h"
#include "Device/Mask/Polygon.h"
#include "Device/Mask/Rectangle.h"
#include "Device/Resolution/ConvolutionDetectorResolution.h"
#include "Device/Resolution/ResolutionFunction2DGaussian.h"
#include "GUI/Model/Axis/BasicAxisItem.h"
#include "GUI/Model/Beam/BeamAngleItems.h"
#include "GUI/Model/Beam/BeamWavelengthItem.h"
#include "GUI/Model/Beam/GrazingScanItem.h"
#include "GUI/Model/Beam/SourceItems.h"
#include "GUI/Model/Descriptor/DistributionItems.h"
#include "GUI/Model/Detector/OffspecDetectorItem.h"
#include "GUI/Model/Detector/RectangularDetectorItem.h"
#include "GUI/Model/Detector/ResolutionFunctionItems.h"
#include "GUI/Model/Detector/SphericalDetectorItem.h"
#include "GUI/Model/Device/BackgroundItems.h"
#include "GUI/Model/Device/InstrumentItems.h"
#include "GUI/Model/Device/InstrumentModel.h"
#include "GUI/Support/Data/SimulationOptionsItem.h"
#include "Param/Distrib/Distributions.h"
#include "Resample/Options/SimulationOptions.h"
#include "Sim/Background/ConstantBackground.h"
#include "Sim/Background/PoissonBackground.h"
#include "Sim/Scan/AlphaScan.h"
#include "Sim/Scan/QzScan.h" // yet unused -> TODO support QzScans!
#include "Sim/Simulation/includeSimulations.h"

namespace {

void setMaskContainer(MaskContainerItem* destMaskItems, const IDetector& detector, double scale)
{
    const auto* detectorMask = detector.detectorMask();
    for (size_t i_mask = 0; i_mask < detectorMask->numberOfMasks(); ++i_mask) {
        const MaskPattern* pat = detectorMask->patternAt(i_mask);
        IShape2D* shape = pat->shape;
        bool mask_value = pat->doMask;

        if (const auto* ellipse = dynamic_cast<const Ellipse*>(shape)) {
            auto* ellipseItem = new EllipseItem();
            ellipseItem->setXCenter(scale * ellipse->getCenterX());
            ellipseItem->setYCenter(scale * ellipse->getCenterY());
            ellipseItem->setXRadius(scale * ellipse->radiusX());
            ellipseItem->setYRadius(scale * ellipse->radiusY());
            ellipseItem->setAngle(scale * ellipse->getTheta());
            ellipseItem->setMaskValue(mask_value);
            // TODO: why prepend the mask, instead of appending it?
            destMaskItems->insertMask(0, ellipseItem);
        }

        else if (const auto* rectangle = dynamic_cast<const Rectangle*>(shape)) {
            auto* rectangleItem = new RectangleItem();
            rectangleItem->setXLow(scale * rectangle->getXlow());
            rectangleItem->setYLow(scale * rectangle->getYlow());
            rectangleItem->setXUp(scale * rectangle->getXup());
            rectangleItem->setYUp(scale * rectangle->getYup());
            rectangleItem->setMaskValue(mask_value);
            // TODO: why prepend the mask, instead of appending it?
            destMaskItems->insertMask(0, rectangleItem);
        }

        else if (const auto* polygon = dynamic_cast<const Polygon*>(shape)) {
            auto* polygonItem = new PolygonItem();
            std::vector<double> xpos, ypos;
            polygon->getPoints(xpos, ypos);
            for (size_t i_point = 0; i_point < xpos.size(); ++i_point)
                polygonItem->addPoint(scale * xpos[i_point], scale * ypos[i_point]);

            polygonItem->setMaskValue(mask_value);
            polygonItem->setIsClosed(true);
            // TODO: why prepend the mask, instead of appending it?
            destMaskItems->insertMask(0, polygonItem);
        }

        else if (const auto* vline = dynamic_cast<const VerticalLine*>(shape)) {
            auto* lineItem = new VerticalLineItem();
            lineItem->setPosX(scale * vline->getXpos());
            lineItem->setMaskValue(mask_value);
            // TODO: why prepend the mask, instead of appending it?
            destMaskItems->insertMask(0, lineItem);
        }

        else if (const auto* hline = dynamic_cast<const HorizontalLine*>(shape)) {
            auto* lineItem = new HorizontalLineItem();
            lineItem->setPosY(scale * hline->getYpos());
            lineItem->setMaskValue(mask_value);
            // TODO: why prepend the mask, instead of appending it?
            destMaskItems->insertMask(0, lineItem);
        }

        else if (const auto* plane = dynamic_cast<const InfinitePlane*>(shape)) {
            Q_UNUSED(plane);
            auto* planeItem = new MaskAllItem();
            planeItem->setMaskValue(mask_value);
            destMaskItems->addMask(planeItem);
        }

        else
            throw std::runtime_error(
                "Cannot convert detector mask from core to GUI: Unknown shape");
    }

    if (detector.hasExplicitRegionOfInterest()) {
        const auto xBounds = detector.regionOfInterestBounds(0);
        const auto yBounds = detector.regionOfInterestBounds(1);

        auto* roiItem = new RegionOfInterestItem();
        roiItem->setXLow(scale * xBounds.first);
        roiItem->setYLow(scale * yBounds.first);
        roiItem->setXUp(scale * xBounds.second);
        roiItem->setYUp(scale * yBounds.second);
        destMaskItems->addMask(roiItem);
    }
}

void setDetectorMasks(DetectorItem* detector_item, const IDetector& detector)
{
    if ((detector.detectorMask() && detector.detectorMask()->hasMasks())
        || detector.hasExplicitRegionOfInterest()) {
        const double scale = 1.0 / detector_item->axesToCoreUnitsFactor();
        setMaskContainer(&detector_item->maskItems(), detector, scale);
    }
}

void setDistributionTypeAndPars(BeamDistributionItem* pdi, const IDistribution1D* d)
{
    const double factor = 1 / pdi->scaleFactor();

    if (const auto* dd = dynamic_cast<const DistributionGate*>(d)) {
        auto* item = pdi->setDistributionItemType<DistributionGateItem>();
        item->setRange(factor * dd->min(), factor * dd->max());
    } else if (const auto* dd = dynamic_cast<const DistributionLorentz*>(d)) {
        auto* item = pdi->setDistributionItemType<DistributionLorentzItem>();
        item->setMean(factor * dd->mean());
        item->setHwhm(factor * dd->hwhm());
    } else if (const auto* dd = dynamic_cast<const DistributionGaussian*>(d)) {
        auto* item = pdi->setDistributionItemType<DistributionGaussianItem>();
        item->setMean(factor * dd->mean());
        item->setStandardDeviation(factor * dd->getStdDev());
    } else if (const auto* dd = dynamic_cast<const DistributionLogNormal*>(d)) {
        auto* item = pdi->setDistributionItemType<DistributionLogNormalItem>();
        item->setMedian(factor * dd->getMedian());
        item->setScaleParameter(dd->getScalePar());
    } else if (const auto* dd = dynamic_cast<const DistributionCosine*>(d)) {
        auto* item = pdi->setDistributionItemType<DistributionCosineItem>();
        item->setMean(factor * dd->mean());
        item->setHwhm(factor * dd->hwhm());
    } else if (const auto* dd = dynamic_cast<const DistributionTrapezoid*>(d)) {
        auto* item = pdi->setDistributionItemType<DistributionTrapezoidItem>();
        item->setCenter(factor * dd->mean());
        item->setLeftWidth(factor * dd->getLeftWidth());
        item->setMiddleWidth(factor * dd->getMiddleWidth());
        item->setRightWidth(factor * dd->getRightWidth());
    } else
        ASSERT(false);
}

void setDistribution(BeamDistributionItem* pdi, ParameterDistribution par_distr)
{
    setDistributionTypeAndPars(pdi, par_distr.getDistribution());

    DistributionItem* distItem = pdi->distributionItem();

    distItem->setNumberOfSamples((int)par_distr.nDraws());

    if (distItem->hasRelSamplingWidth())
        distItem->setRelSamplingWidth(par_distr.relSamplingWidth());
}

void addDistributionToItem(BeamDistributionItem* pdi, const IDistribution1D* distribution)
{
    if (!pdi)
        return;
    setDistributionTypeAndPars(pdi, distribution);

    DistributionItem* distItem = pdi->distributionItem();

    distItem->setNumberOfSamples((int)distribution->nSamples());
    distItem->setRelSamplingWidth(distribution->relSamplingWidth());
}

void setGISASBeamItem(BeamItem* beam_item, const ScatteringSimulation& simulation)
{
    ASSERT(beam_item);
    const Beam& beam = simulation.beam();

    beam_item->setIntensity(beam.intensity());
    beam_item->setWavelength(beam.wavelength());
    beam_item->setInclinationAngle(Units::rad2deg(beam.alpha_i()));
    beam_item->setAzimuthalAngle(Units::rad2deg(beam.phi_i()));
    beam_item->setFootprint(beam.footprint());

    for (const ParameterDistribution& pd : simulation.paramDistributions()) {
        if (pd.whichParameter() == ParameterDistribution::BeamWavelength)
            setDistribution(beam_item->wavelengthItem(), pd);
        else if (pd.whichParameter() == ParameterDistribution::BeamInclinationAngle)
            setDistribution(beam_item->beamDistributionItem(), pd);
        else if (pd.whichParameter() == ParameterDistribution::BeamAzimuthalAngle)
            setDistribution(beam_item->azimuthalAngleItem(), pd);
        else
            ASSERT(false);
    }
}

void setSphericalDetector(SphericalDetectorItem* detectorItem, const SphericalDetector& detector)
{
    // Axes
    const Scale& phi_axis = detector.axis(0);
    const Scale& alpha_axis = detector.axis(1);

    auto& phiAxisItem = detectorItem->phiAxis();
    phiAxisItem.setNbins(phi_axis.size());
    phiAxisItem.setMin(Units::rad2deg(phi_axis.min()));
    phiAxisItem.setMax(Units::rad2deg(phi_axis.max()));

    auto& alphaAxisItem = detectorItem->alphaAxis();
    alphaAxisItem.setNbins(alpha_axis.size());
    alphaAxisItem.setMin(Units::rad2deg(alpha_axis.min()));
    alphaAxisItem.setMax(Units::rad2deg(alpha_axis.max()));
}

void setRectangularDetector(RectangularDetectorItem* detectorItem,
                            const RectangularDetector& detector)
{
    // Axes
    detectorItem->setXSize(detector.xSize());
    detectorItem->setWidth(detector.width());

    detectorItem->setYSize(detector.ySize());
    detectorItem->setHeight(detector.height());

    detectorItem->setDetectorAlignment(detector.getDetectorArrangment());
    if (detector.getDetectorArrangment() == RectangularDetector::GENERIC) {
        R3 normal = detector.getNormalVector();
        detectorItem->setNormalVector(normal);

        R3 direction = detector.getDirectionVector();
        detectorItem->setDirectionVector(direction);

        detectorItem->setU0(detector.getU0());
        detectorItem->setV0(detector.getV0());
    }

    else if (detector.getDetectorArrangment() == RectangularDetector::PERPENDICULAR_TO_SAMPLE) {
        detectorItem->setDistance(detector.getDistance());
        detectorItem->setU0(detector.getU0());
        detectorItem->setV0(detector.getV0());
    } else if (detector.getDetectorArrangment()
               == RectangularDetector::PERPENDICULAR_TO_DIRECT_BEAM) {
        detectorItem->setDistance(detector.getDistance());
        detectorItem->setU0(detector.getU0());
        detectorItem->setV0(detector.getV0());
    } else if (detector.getDetectorArrangment()
               == RectangularDetector::PERPENDICULAR_TO_REFLECTED_BEAM) {
        detectorItem->setDistance(detector.getDistance());
        detectorItem->setU0(detector.getU0());
        detectorItem->setV0(detector.getV0());
    } else
        ASSERT(false);
}

void setDetectorGeometry(GISASInstrumentItem* instrument_item, const IDetector& detector)
{
    if (const auto* det = dynamic_cast<const SphericalDetector*>(&detector)) {
        auto* item = instrument_item->setDetectorItemType<SphericalDetectorItem>();
        setSphericalDetector(item, *det);
    } else if (const auto* det = dynamic_cast<const RectangularDetector*>(&detector)) {
        auto* item = instrument_item->setDetectorItemType<RectangularDetectorItem>();
        setRectangularDetector(item, *det);
    } else
        ASSERT(false);
}

void setDetectorResolution(DetectorItem* detector_item, const IDetector& detector)
{
    const IDetectorResolution* resfunc = detector.detectorResolution();

    if (!resfunc)
        return;

    if (const auto* convfunc = dynamic_cast<const ConvolutionDetectorResolution*>(resfunc)) {
        if (const auto* resfunc = dynamic_cast<const ResolutionFunction2DGaussian*>(
                convfunc->getResolutionFunction2D())) {
            auto* item =
                detector_item->setResolutionFunctionType<ResolutionFunction2DGaussianItem>();
            const double scale = 1.0 / detector_item->axesToCoreUnitsFactor();
            item->setSigmaX(scale * resfunc->sigmaX());
            item->setSigmaY(scale * resfunc->sigmaY());
        } else {
            throw std::runtime_error("setDetectorResolution -> Error. "
                                     "Unknown detector resolution function");
        }
    } else
        ASSERT(false);
}

void setPolarizer2(InstrumentItem* instrument_item, const PolFilter& analyzer)
{
    instrument_item->setAnalyzerBlochVector(analyzer.BlochVector());
}

void updateDetector(GISASInstrumentItem* instrument_item, const IDetector& detector)
{
    setDetectorGeometry(instrument_item, detector);

    auto* detector_item = instrument_item->detectorItem();

    setDetectorResolution(detector_item, detector);
    setDetectorMasks(detector_item, detector);
    setPolarizer2(instrument_item, detector.analyzer());
}

void setBackground(InstrumentItem* instrument_item, const ISimulation& simulation)
{
    const auto* bg = simulation.background();
    if (const auto* constant_bg = dynamic_cast<const ConstantBackground*>(bg)) {
        auto* constant_bg_item = instrument_item->setBackgroundItemType<ConstantBackgroundItem>();
        double value = constant_bg->backgroundValue();
        constant_bg_item->setBackgroundValue(value);
    } else if (dynamic_cast<const PoissonBackground*>(bg))
        instrument_item->setBackgroundItemType<PoissonBackgroundItem>();
}

GISASInstrumentItem* createGISASInstrumentItem(const ScatteringSimulation& simulation)
{
    auto* result = new GISASInstrumentItem();
    setGISASBeamItem(result->beamItem(), simulation);
    result->setPolarizerBlochVector(simulation.beam().polVector());
    updateDetector(result, simulation.detector());
    result->setWithPolarizer(true);
    result->setWithAnalyzer(true);
    return result;
}

OffspecInstrumentItem* createOffspecInstrumentItem(const OffspecSimulation& simulation)
{
    auto* result = new OffspecInstrumentItem();

    const IBeamScan* scan = simulation.scan();
    result->scanItem()->setScan(scan);

    const OffspecDetector& detector = simulation.detector();
    OffspecDetectorItem* detectorItem = result->detectorItem();

    const Scale& phi_axis = detector.axis(0);
    const Scale& alpha_axis = detector.axis(1);

    auto& phiAxisItem = detectorItem->phiAxis();
    phiAxisItem.setNbins(phi_axis.size());
    phiAxisItem.setMin(Units::rad2deg(phi_axis.min()));
    phiAxisItem.setMax(Units::rad2deg(phi_axis.max()));

    auto& alphaAxisItem = detectorItem->alphaAxis();
    alphaAxisItem.setNbins(alpha_axis.size());
    alphaAxisItem.setMin(Units::rad2deg(alpha_axis.min()));
    alphaAxisItem.setMax(Units::rad2deg(alpha_axis.max()));

    setPolarizer2(result, detector.analyzer());
    result->setWithPolarizer(true);
    result->setWithAnalyzer(true);
    return result;
}

SpecularInstrumentItem* createSpecularInstrumentItem(const SpecularSimulation& simulation)
{
    auto* result = new SpecularInstrumentItem();

    const IBeamScan* scan = simulation.scan();
    result->scanItem()->setScan(scan);

    if (const auto* s2 = dynamic_cast<const AlphaScan*>(scan)) {
        if (const IDistribution1D* distribution = s2->wavelengthDistribution())
            addDistributionToItem(result->scanItem()->wavelengthItem(), distribution);
        if (const IDistribution1D* distribution = s2->angleDistribution())
            addDistributionToItem(result->scanItem()->grazingScanItem(), distribution);
    }
    return result;
}

} // namespace


InstrumentModel* GUI::FromCore::itemizeInstruments(const ISimulation& simulation)
{
    InstrumentItem* item;

    if (const auto* gisasSimulation = dynamic_cast<const ScatteringSimulation*>(&simulation))
        item = createGISASInstrumentItem(*gisasSimulation);
    else if (const auto* offspecSimulation = dynamic_cast<const OffspecSimulation*>(&simulation))
        item = createOffspecInstrumentItem(*offspecSimulation);
    else if (const auto* spec_simulation = dynamic_cast<const SpecularSimulation*>(&simulation))
        item = createSpecularInstrumentItem(*spec_simulation);
    else
        ASSERT(false);

    setBackground(item, simulation);

    auto* model = new InstrumentModel;
    model->emplace_back(item);
    return model;
}

SimulationOptionsItem* GUI::FromCore::itemizeOptions(const ISimulation& simulation)
{
    auto* result = new SimulationOptionsItem;

    if (simulation.options().isIntegrate())
        result->setUseMonteCarloIntegration(
            static_cast<unsigned>(simulation.options().getMcPoints()));
    else
        result->setUseAnalytical();

    result->setUseAverageMaterials(simulation.options().useAvgMaterials());
    result->setIncludeSpecularPeak(simulation.options().includeSpecular());

    return result;
}
