//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Correlation/Profiles2D.cpp
//! @brief     Implements class interface Profile2D and children thereof.
//!
//! @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 "Sample/Correlation/Profiles2D.h"
#include "Base/Math/Bessel.h"
#include "Base/Math/Integrator.h"
#include "Base/Util/Assert.h"
#include <limits>

//  ************************************************************************************************
//  interface Profile2D
//  ************************************************************************************************

Profile2D::Profile2D(double omega_x, double omega_y, double gamma)
    : m_omega_x(omega_x)
    , m_omega_y(omega_y)
    , m_gamma(gamma)
{
}

void Profile2D::validate() const
{
    if (m_omega_x <= 0)
        throw std::runtime_error("omega_x <= 0");
    if (m_omega_y <= 0)
        throw std::runtime_error("omega_y <= 0");
    m_validated = true;
}

double Profile2D::sumsq(double qx, double qy) const
{
    ASSERT(m_validated);
    return qx * qx * m_omega_x * m_omega_x + qy * qy * m_omega_y * m_omega_y;
}

//  ************************************************************************************************
//  class Profile2DCauchy
//  ************************************************************************************************

Profile2DCauchy::Profile2DCauchy(double omega_x, double omega_y, double gamma)
    : Profile2D(omega_x, omega_y, gamma)
{
    Profile2D::validate();
}

Profile2DCauchy* Profile2DCauchy::clone() const
{
    return new Profile2DCauchy(m_omega_x, m_omega_y, m_gamma);
}

double Profile2DCauchy::standardizedFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    return std::pow(1.0 + sumsq(qx, qy), -1.5);
}

double Profile2DCauchy::decayFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    double sum_sq = qx * qx * m_omega_x * m_omega_x + qy * qy * m_omega_y * m_omega_y;
    return (2 * pi) * m_omega_x * m_omega_y * std::pow(1.0 + sum_sq, -1.5);
}

std::unique_ptr<Distribution2DSampler> Profile2DCauchy::createSampler() const
{
    return std::make_unique<Distribution2DCauchySampler>(m_omega_x, m_omega_y);
}

//  ************************************************************************************************
//  class Profile2DGauss
//  ************************************************************************************************

Profile2DGauss::Profile2DGauss(double omega_x, double omega_y, double gamma)
    : Profile2D(omega_x, omega_y, gamma)
{
    Profile2D::validate();
}

Profile2DGauss* Profile2DGauss::clone() const
{
    return new Profile2DGauss(m_omega_x, m_omega_y, m_gamma);
}

double Profile2DGauss::standardizedFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    return std::exp(-sumsq(qx, qy) / 2);
}

double Profile2DGauss::decayFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    return (2 * pi) * m_omega_x * m_omega_y * std::exp(-sumsq(qx, qy) / 2.0);
}

std::unique_ptr<Distribution2DSampler> Profile2DGauss::createSampler() const
{
    return std::make_unique<Distribution2DGaussSampler>(m_omega_x, m_omega_y);
}

//  ************************************************************************************************
//  class Profile2DGate
//  ************************************************************************************************

Profile2DGate::Profile2DGate(double omega_x, double omega_y, double gamma)
    : Profile2D(omega_x, omega_y, gamma)
{
    Profile2D::validate();
}

Profile2DGate* Profile2DGate::clone() const
{
    return new Profile2DGate(m_omega_x, m_omega_y, m_gamma);
}

double Profile2DGate::standardizedFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    double scaled_q = std::sqrt(sumsq(qx, qy));
    return Math::Bessel::J1c(scaled_q) * 2.0;
}

double Profile2DGate::decayFT2D(double, double) const
{
    ASSERT_NEVER; // not yet needed, not yet implemented
}

std::unique_ptr<Distribution2DSampler> Profile2DGate::createSampler() const
{
    return std::make_unique<Distribution2DGateSampler>(m_omega_x, m_omega_y);
}

//  ************************************************************************************************
//  class Profile2DCone
//  ************************************************************************************************

Profile2DCone::Profile2DCone(double omega_x, double omega_y, double gamma)
    : Profile2D(omega_x, omega_y, gamma)
{
    Profile2D::validate();
}

Profile2DCone* Profile2DCone::clone() const
{
    return new Profile2DCone(m_omega_x, m_omega_y, m_gamma);
}

double Profile2DCone::standardizedFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    double scaled_q = std::sqrt(sumsq(qx, qy));
    if (scaled_q < std::numeric_limits<double>::epsilon())
        return 1.0 - 3.0 * scaled_q * scaled_q / 40.0;
    // second part of the integrand: \f$u^2\cdot J_0(u)\f$
    double integral = RealIntegrator().integrate(
        [](double x) -> double { return x * x * Math::Bessel::J0(x); }, 0.0, scaled_q);
    return 6.0 * (Math::Bessel::J1c(scaled_q) - integral / scaled_q / scaled_q / scaled_q);
}

double Profile2DCone::decayFT2D(double, double) const
{
    ASSERT_NEVER; // not yet needed, not yet implemented
}

std::unique_ptr<Distribution2DSampler> Profile2DCone::createSampler() const
{
    return std::make_unique<Distribution2DConeSampler>(m_omega_x, m_omega_y);
}

//  ************************************************************************************************
//  class Profile2DVoigt
//  ************************************************************************************************

Profile2DVoigt::Profile2DVoigt(double omega_x, double omega_y, double gamma, double eta)
    : Profile2D(omega_x, omega_y, gamma)
    , m_eta(eta)
{
    Profile2DVoigt::validate();
}

Profile2DVoigt* Profile2DVoigt::clone() const
{
    return new Profile2DVoigt(m_omega_x, m_omega_y, m_gamma, m_eta);
}

void Profile2DVoigt::validate() const
{
    Profile2D::validate();
    if (m_eta < 0 || m_eta > 1)
        throw std::runtime_error("eta not in [0,1]");
    m_validated = true;
}

double Profile2DVoigt::standardizedFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    double sum_sq = sumsq(qx, qy);
    return m_eta * std::exp(-sum_sq / 2) + (1.0 - m_eta) * std::pow(1.0 + sum_sq, -1.5);
}

double Profile2DVoigt::decayFT2D(double qx, double qy) const
{
    ASSERT(m_validated);
    double sum_sq = qx * qx * m_omega_x * m_omega_x + qy * qy * m_omega_y * m_omega_y;
    return (2 * pi) * m_omega_x * m_omega_y
           * (m_eta * std::exp(-sum_sq / 2.0) + (1.0 - m_eta) * std::pow(1.0 + sum_sq, -1.5));
}

std::unique_ptr<Distribution2DSampler> Profile2DVoigt::createSampler() const
{
    throw std::runtime_error("Profile2DVoigt::createSampler not yet implemented."
                             " Please contact the maintainers");
}
