//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Base/Math/Integrator.cpp
//! @brief     Implements classes RealIntegrator, ComplexIntegrator.
//!
//! @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 "Base/Math/Integrator.h"
#include "Base/Util/Assert.h"

namespace {

bool isInteger(double v)
{
    return std::trunc(v) == v;
}

// These parameter names are read from user script. On change, upd the docs and examples.
std::string max_subintervals_name = "max_subintervals";
std::string subdivision_depth_name = "subdivision_depth";
std::string abs_tolerance_name = "abs_tolerance";
std::string rel_tolerance_name = "rel_tolerance";
std::string GK_rule_name = "GK_rule";

} // namespace

//-------------------------------------------------------------------------------------------------

RealIntegrator::RealIntegrator(const IntegratorOptions& options)
    : m_options(options)
{
    const auto& map = m_options.pars;

    if (options.type == Integr::AdaptiveGaussKronrod) {
        size_t max_subintervals = 200;
        auto pos = map.find(::max_subintervals_name);
        if (pos != map.end()) {
            if (!::isInteger(pos->second) || pos->second < 1)
                throw std::runtime_error("\"" + ::max_subintervals_name
                                         + "\" parameter must be integer >= 1");
            max_subintervals = (size_t)pos->second;
        }
        m_integrator = std::make_unique<GKAdaptiveIntegrator>(max_subintervals);

    } else if (options.type == Integr::GaussKronrod)
        m_integrator = std::make_unique<GKIntegrator>();

    else if (options.type == Integr::Romberg) {
        size_t subdivision_depth = 5;
        auto pos = map.find(::subdivision_depth_name);
        if (pos != map.end()) {
            if (!::isInteger(pos->second) || pos->second < 1)
                throw std::runtime_error("\"" + ::subdivision_depth_name
                                         + "\" parameter must be integer >= 1");
            subdivision_depth = (size_t)pos->second;
        }
        m_integrator = std::make_unique<RombergIntegrator>(subdivision_depth);

    } else
        throw std::runtime_error("Integrator \"" + options.type + "\" does not exist.");
}

double RealIntegrator::integrate(const std::function<double(double)>& f, double lmin, double lmax)
{
    const auto& map = m_options.pars;

    if (auto* integr = dynamic_cast<GKAdaptiveIntegrator*>(m_integrator.get())) {

        auto pos = map.find(::abs_tolerance_name);
        double epsabs = pos != map.end() ? pos->second : 1e-9;

        pos = map.find(::rel_tolerance_name);
        double epsrel = pos != map.end() ? pos->second : 1e-7;

        pos = map.find(::GK_rule_name);
        int key = GSL_INTEG_GAUSS31;
        if (pos != map.end())
            if (::isInteger(pos->second) && pos->second >= 1 && pos->second <= 6)
                key = (int)pos->second;

        return integr->integrate(f, lmin, lmax, epsabs, epsrel, key, integr->max_subintervals);

    } else if (auto* integr = dynamic_cast<GKIntegrator*>(m_integrator.get())) {
        auto pos = map.find(::abs_tolerance_name);
        double epsabs = pos != map.end() ? pos->second : 1e-9;

        pos = map.find(::rel_tolerance_name);
        double epsrel = pos != map.end() ? pos->second : 1e-7;

        return integr->integrate(f, lmin, lmax, epsabs, epsrel);

    } else if (auto* integr = dynamic_cast<RombergIntegrator*>(m_integrator.get())) {
        auto pos = map.find(::abs_tolerance_name);
        double epsabs = pos != map.end() ? pos->second : 1e-9;

        pos = map.find(::rel_tolerance_name);
        double epsrel = pos != map.end() ? pos->second : 1e-7;

        return integr->integrate(f, lmin, lmax, epsabs, epsrel);

    } else
        ASSERT_NEVER;
}

//-------------------------------------------------------------------------------------------------

ComplexIntegrator::ComplexIntegrator(const IntegratorOptions& options)
    : m_realPart(options)
    , m_imagPart(options)
{
}

complex_t ComplexIntegrator::integrate(const std::function<complex_t(double)>& f, double lmin,
                                       double lmax)
{
    return {m_realPart.integrate([f](double x) { return f(x).real(); }, lmin, lmax),
            m_imagPart.integrate([f](double x) { return f(x).imag(); }, lmin, lmax)};
}
