//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sim/Evaluate/EvaluateScattering.cpp
//! @brief     Implements  function Evaluate::scatteredContribution.
//!
//! @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/Evaluate/EvaluateScattering.h"
#include "Base/Math/IntegratorMCMiser.h"
#include "Base/Util/Assert.h"
#include "Resample/Sample/ReSample.h"
#include "Sample/Aggregate/Struct2D.h"
#include "Sample/Aggregate/Struct3D.h"
#include "Sample/Element/DiffuseElement.h"
#include "Sample/Flux/Flux.h"
#include "Sample/Option/SimulationOptions.h"
#include "Sim/Evaluate/EvaluateRoughnessScattering.h"
#include <functional>

namespace {

using F_t = std::function<double(const DiffuseElement& ele)>;
struct MCPars {
    const DiffuseElement& ele;
    const F_t F;
};

double singleEvaluate(const double* fractions, const void* params)
{
    const auto* mcpars = static_cast<const MCPars*>(params);
    const DiffuseElement& ele = mcpars->ele;
    DiffuseElement point_ele = ele.pointElement(fractions[0], fractions[1]);
    return ele.integrationFactor(fractions[0], fractions[1]) * mcpars->F(point_ele);
}

//! Performs a Monte Carlo integration over the bin for the evaluation of the intensity.
double MCIntegratedEvaluate(const MCPars& mcpars, int nPoints)
{
    if (mcpars.ele.solidAngle() <= 0)
        throw std::runtime_error(
            "Monte-Carlo integration probably makes no sense as detector pixel is probably a dot");
    double min_array[] = {0.0, 0.0};
    double max_array[] = {1.0, 1.0};
    std::function<double(double* fractions, void* params)> f =
        [](double* fractions, void* params) -> double { return singleEvaluate(fractions, params); };
    return IntegratorMCMiser(2).integrate(min_array, max_array, f, (void*)&mcpars, nPoints);
}

double scatteringIntensity(const DiffuseElement& ele, const F_t& F,
                           const SimulationOptions& options)
{
    if (options.isIntegrate())
        return ::MCIntegratedEvaluate(MCPars(ele, F), options.mcPoints());
    return F(ele);
}

double scatteringWeight(const Struct* s)
{
    if (const auto* s2d = dynamic_cast<const Struct2D*>(s))
        return s2d->areaNumberDensity() * s->coverage();
    if (const auto* s3d = dynamic_cast<const Struct3D*>(s))
        return s3d->volumeNumberDensity() * s3d->thickness() * s->coverage();
    ASSERT_NEVER;
}

} // namespace


// The normalization of the calculated scattering intensities is:
// For nanoparticles: rho * (scattering cross-section/scattering particle)
// For roughness: (scattering cross-section of area S)/S
// For specular peak: |R|^2 * sin(alpha_i) / solid_angle
// This allows them to be added and normalized together to the beam afterwards
double Evaluate::scatteredContribution(const ReSample& re_sample, DiffuseElement& ele,
                                       const SimulationOptions& options, bool polarized)
{
    const Fluxes fluxes_in = re_sample.fluxesIn(ele.getKi(), polarized);
    const Fluxes fluxes_out = re_sample.fluxesOut(ele.meanKf(), polarized);
    ele.setFluxes(&fluxes_in, &fluxes_out);

    double intensity = 0;

    for (const auto& s : re_sample.structs()) {
        const Struct* sp = s.get();
        if (polarized)
            intensity +=
                ::scatteringWeight(sp)
                * ::scatteringIntensity(
                    ele, [sp](const DiffuseElement& ele) { return sp->polarizedScattering(ele); },
                    options);
        else
            intensity +=
                ::scatteringWeight(sp)
                * ::scatteringIntensity(
                    ele, [sp](const DiffuseElement& ele) { return sp->scalarScattering(ele); },
                    options);
    }

    if (re_sample.hasRoughness())
        intensity += Evaluate::roughnessScattering(re_sample, ele);

    return intensity;
}
