//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sim/Scan/AlphaScan.cpp
//! @brief     Implements class AlphaScan.
//!
//! @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/Scan/AlphaScan.h"
#include "Base/Axis/MakeScale.h"
#include "Base/Axis/Scale.h"
#include "Base/Math/Numeric.h"
#include "Base/Vector/GisasDirection.h"
#include "Device/Beam/IFootprint.h"
#include "Device/Pol/PolFilter.h"
#include "Param/Distrib/Distributions.h"
#include "Param/Distrib/ParameterSample.h"
#include "Resample/Element/SpecularElement.h"
#include <algorithm> // is_sorted
#include <numbers>

using std::numbers::pi;

namespace {

std::vector<ParameterSample> drawDistribution(IDistribution1D* distrib, double point)
{
    if (!distrib)
        return {{point, 1}};
    distrib->setMean(point);
    return distrib->distributionSamples();
}

} // namespace


AlphaScan::AlphaScan(const Scale& alpha_axis)
    : PhysicalScan(alpha_axis.clone())
{
    const std::vector<double> axis_values = m_axis->binCenters();
    if (!std::is_sorted(axis_values.begin(), axis_values.end()))
        throw std::runtime_error("AlphaScan: angle values are not "
                                 "sorted in ascending order.");
    if (axis_values.front() < 0)
        throw std::runtime_error("AlphaScan: negative angles.");
    if (axis_values.back() > std::numbers::pi / 2)
        throw std::runtime_error("AlphaScan: angles beyond normal.");

    m_beams.clear();
    for (size_t i = 0; i < nScan(); i++) {
        auto* beam = new Beam(defaultIntensity, defaultWavelength, m_axis->binCenter(i));
        m_beams.push_back(beam);
    }
}

AlphaScan::AlphaScan(std::vector<double> points)
    : AlphaScan(ListScan("alpha_i (rad)", std::move(points)))
{
}

AlphaScan::AlphaScan(int nbins, double alpha_i_min, double alpha_i_max)
    : AlphaScan(EquiScan("alpha_i (rad)", nbins, alpha_i_min, alpha_i_max))
{
}

AlphaScan::~AlphaScan() = default;

AlphaScan* AlphaScan::clone() const
{
    auto* result = new AlphaScan(*m_axis);
    copyPhysicalScan(result);
    result->setAlphaOffset(alphaOffset());
    return result;
}

std::vector<SpecularElement> AlphaScan::generateElements() const
{
    std::vector<SpecularElement> result;
    result.reserve(nScan() * nDistributionSamples());

    for (size_t i = 0; i < m_axis->size(); ++i) {
        const std::vector<ParameterSample> alphaDistrib =
            drawDistribution(m_alpha_distrib.get(), inclinationAt(i) + m_alpha_offset);
        const std::vector<ParameterSample> lambdaDistrib =
            drawDistribution(m_lambda_distrib.get(), wavelengthAt(i));
        for (auto j : alphaDistrib) {
            const double alpha = j.value;
            for (auto k : lambdaDistrib) {
                const double lambda = k.value;
                const bool computable = lambda >= 0 && alpha >= 0 && alpha <= (pi / 2);
                const double weight = j.weight * k.weight;
                const double footprint = footprintAt(i) ? footprintAt(i)->calculate(alpha) : 1;
                const R3 veck = vecOfLambdaAlphaPhi(lambda, -alpha);
                result.emplace_back(i, computable, intensityAt(i), weight, footprint,
                                    polarizerMatrixAt(i), analyzerMatrix(), veck);
            }
        }
    }
    return result;
}
