//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sim/Simulation/OffspecSimulation.cpp
//! @brief     Implements class OffspecSimulation.
//!
//! @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 "Sim/Simulation/OffspecSimulation.h"
#include "Base/Axis/Frame.h"
#include "Base/Axis/Pixel.h"
#include "Base/Axis/Scale.h"
#include "Base/Math/IntegratorCollection.h"
#include "Base/Progress/ProgressHandler.h"
#include "Base/Util/Assert.h"
#include "Device/Beam/Footprint.h"
#include "Device/Data/Datafield.h"
#include "Device/Detector/OffspecDetector.h"
#include "Sample/Element/DiffuseElement.h"
#include "Sample/Element/ScanElement.h"
#include "Sample/Option/SimulationOptions.h"
#include "Sim/Background/Background.h"
#include "Sim/Evaluate/EvaluateScattering.h"
#include "Sim/Evaluate/EvaluateSpecularGisas.h"
#include "Sim/Scan/PhysicalScan.h"

OffspecSimulation::OffspecSimulation(const PhysicalScan& scan, const Sample& sample,
                                     const OffspecDetector& detector)
    : Simulation(sample)
    , m_scan(scan.clone())
    , m_detector(detector.clone())
{
}

OffspecSimulation::~OffspecSimulation() = default;

//... Overridden executors:

void OffspecSimulation::initScanElementVector()
{
    m_eles = m_scan->generateElements();
}

void OffspecSimulation::runComputation(const ReSample& re_sample, size_t i, double weight)
{
    const size_t Nalp = m_detector->alphaAxis().size();
    size_t j = i / Nalp; // index in scan (including distribution sampling)
    size_t k = i % Nalp; // alpha-index in detector

    ASSERT(m_cache.size() / Nalp == m_eles.size());

    ScanElement& scan_ele = *(m_eles.begin() + static_cast<long>(j));
    const double alpha_i = scan_ele.alpha();
    const double lambda_i = scan_ele.lambda();
    const double phi_i = scan_ele.phi();

    const double phi_min = m_detector->phiRange().min();
    const double phi_max = m_detector->phiRange().max();

    const bool containsSpecular = k == m_detector->aplhaIndexOfSpecular(alpha_i, phi_i);

    double intensity =
        RealIntegrator(options().offspecIntegratorOptions())
            .integrate(
                [&](double phi) -> double {
                    std::unique_ptr<const Pixel> pixel(m_detector->createPixel(k, phi));
                    DiffuseElement diff_ele(lambda_i, alpha_i, phi_i, pixel.get(),
                                            m_scan->polarizerMatrixAt(scan_ele.i_out()),
                                            m_detector->analyzerMatrix());
                    return Evaluate::scatteredContribution(re_sample, diff_ele, options(),
                                                           doItPolarized())
                           * diff_ele.solidAngle();
                },
                phi_min, phi_max);

    if (containsSpecular && options().includeSpecular()) {
        std::unique_ptr<const Pixel> pixel(m_detector->createPixel(k, phi_i));
        DiffuseElement diff_ele(lambda_i, alpha_i, phi_i, pixel.get(),
                                m_scan->polarizerMatrixAt(scan_ele.i_out()),
                                m_detector->analyzerMatrix());
        intensity += Evaluate::gisasSpecularContribution(re_sample, diff_ele, doItPolarized())
                     * diff_ele.solidAngle();
    }

    const double sin_alpha_i = std::abs(std::sin(alpha_i));
    if (sin_alpha_i == 0.0)
        intensity = 0;
    else
        intensity *= scan_ele.beamIntensity() * scan_ele.footprint() / sin_alpha_i;

    m_cache[i] += intensity * weight * scan_ele.weight();

    progress().incrementDone(1);
}

//... Overridden getters:

bool OffspecSimulation::forcePolarized() const
{
    return m_detector->analyzer() != nullptr;
}

size_t OffspecSimulation::nElements() const
{
    return m_detector->alphaAxis().size() * m_scan->nElements();
}

Datafield OffspecSimulation::packResult() const
{
    const size_t ns = m_scan->nScan();
    const size_t nslong = m_scan->nElements();

    const size_t nalp = m_detector->alphaAxis().size();
    std::vector<double> out(ns * nalp, 0.);

    for (size_t j = 0; j < nslong; ++j) {
        for (size_t ia = 0; ia < nalp; ++ia) {

            // accumulate distribution points to the scan point
            const ScanElement& ele = m_eles.at(j);
            const size_t j_real = ele.i_out();
            out[ia * ns + j_real] += m_cache[j * nalp + ia];
        }
    }

    // TODO restore resolution m_detector->applyDetectorResolution(&detector_image);

    if (background())
        for (double& o : out)
            o = background()->perturbed(o);

    std::vector<const Scale*> axes{m_scan->coordinateAxis()->clone(),
                                   m_detector->alphaAxis().clone()};
    return {axes, out};
}
