//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Correlation/Profiles1D.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/Profiles1D.h"
#include "Base/Math/Functions.h"
#include "Base/Util/Assert.h"
#include <limits>
#include <numbers>

using std::numbers::pi;

namespace {

const double CosineDistributionFactor = 1.0 / 3.0 - 2.0 / pi / pi;

} // namespace


//  ************************************************************************************************
//  interface Profile1D
//  ************************************************************************************************

Profile1D::Profile1D(double omega)
    : m_omega(omega)
{
}

void Profile1D::validate() const
{
    if (m_omega <= 0)
        throw std::runtime_error("omega <= 0");
    m_validated = true;
}

//  ************************************************************************************************
//  class Profile1DCauchy
//  ************************************************************************************************

Profile1DCauchy::Profile1DCauchy(double omega)
    : Profile1D(omega)
{
    Profile1D::validate();
}

Profile1DCauchy* Profile1DCauchy::clone() const
{
    return new Profile1DCauchy(m_omega);
}

double Profile1DCauchy::standardizedFT(double q) const
{
    double sum_sq = q * q * m_omega * m_omega;
    return 1.0 / (1.0 + sum_sq);
}

double Profile1DCauchy::decayFT(double q) const
{
    double sum_sq = q * q * m_omega * m_omega;
    return m_omega * 2.0 / (1.0 + sum_sq);
}

double Profile1DCauchy::qSecondDerivative() const
{
    return 2.0 * m_omega * m_omega;
}

std::unique_ptr<Distribution1DSampler> Profile1DCauchy::createSampler() const
{
    return std::make_unique<Distribution1DCauchySampler>(1 / m_omega);
}

//  ************************************************************************************************
//  class Profile1DGauss
//  ************************************************************************************************

Profile1DGauss::Profile1DGauss(double omega)
    : Profile1D(omega)
{
    Profile1D::validate();
}

Profile1DGauss* Profile1DGauss::clone() const
{
    return new Profile1DGauss(m_omega);
}

double Profile1DGauss::standardizedFT(double q) const
{
    double sum_sq = q * q * m_omega * m_omega;
    return std::exp(-sum_sq / 2.0);
}

double Profile1DGauss::decayFT(double q) const
{
    double sum_sq = q * q * m_omega * m_omega;
    return m_omega * std::sqrt((2 * pi)) * std::exp(-sum_sq / 2.0);
}

double Profile1DGauss::qSecondDerivative() const
{
    return m_omega * m_omega;
}

std::unique_ptr<Distribution1DSampler> Profile1DGauss::createSampler() const
{
    return std::make_unique<Distribution1DGaussSampler>(0.0, m_omega);
}

//  ************************************************************************************************
//  class Profile1DGate
//  ************************************************************************************************

Profile1DGate::Profile1DGate(double omega)
    : Profile1D(omega)
{
    Profile1D::validate();
}

double Profile1DGate::standardizedFT(double q) const
{
    return Math::sinc(q * m_omega);
}

double Profile1DGate::decayFT(double) const
{
    ASSERT_NEVER; // not yet needed, not yet implemented
}

double Profile1DGate::qSecondDerivative() const
{
    return m_omega * m_omega / 3.0;
}

Profile1DGate* Profile1DGate::clone() const
{
    return new Profile1DGate(m_omega);
}

std::unique_ptr<Distribution1DSampler> Profile1DGate::createSampler() const
{
    return std::make_unique<Distribution1DGateSampler>(-m_omega, m_omega);
}

//  ************************************************************************************************
//  class Profile1DTriangle
//  ************************************************************************************************

Profile1DTriangle::Profile1DTriangle(double omega)
    : Profile1D(omega)
{
    Profile1DTriangle::validate();
}

Profile1DTriangle* Profile1DTriangle::clone() const
{
    return new Profile1DTriangle(m_omega);
}

double Profile1DTriangle::standardizedFT(double q) const
{
    double sincqw2 = Math::sinc(q * m_omega / 2.0);
    return sincqw2 * sincqw2;
}

double Profile1DTriangle::decayFT(double q) const
{
    double sincqw2 = Math::sinc(q * m_omega / 2.0);
    return m_omega * sincqw2 * sincqw2;
}

double Profile1DTriangle::qSecondDerivative() const
{
    return m_omega * m_omega / 6.0;
}

std::unique_ptr<Distribution1DSampler> Profile1DTriangle::createSampler() const
{
    return std::make_unique<Distribution1DTriangleSampler>(m_omega);
}

//  ************************************************************************************************
//  class Profile1DCosine
//  ************************************************************************************************

Profile1DCosine::Profile1DCosine(double omega)
    : Profile1D(omega)
{
    Profile1D::validate();
}

Profile1DCosine* Profile1DCosine::clone() const
{
    return new Profile1DCosine(m_omega);
}

double Profile1DCosine::standardizedFT(double q) const
{
    double qw = std::abs(q * m_omega);
    if (std::abs(1.0 - qw * qw / pi / pi) < std::numeric_limits<double>::epsilon())
        return 0.5;
    return Math::sinc(qw) / (1.0 - qw * qw / pi / pi);
}

double Profile1DCosine::decayFT(double) const
{
    ASSERT_NEVER; // not yet needed, not yet implemented
}

double Profile1DCosine::qSecondDerivative() const
{
    return CosineDistributionFactor * m_omega * m_omega;
}

std::unique_ptr<Distribution1DSampler> Profile1DCosine::createSampler() const
{
    return std::make_unique<Distribution1DCosineSampler>(m_omega);
}

//  ************************************************************************************************
//  class Profile1DVoigt
//  ************************************************************************************************

Profile1DVoigt::Profile1DVoigt(double omega, double eta)
    : Profile1D(omega)
    , m_eta(eta)
{
    Profile1DVoigt::validate();
}

Profile1DVoigt* Profile1DVoigt::clone() const
{
    return new Profile1DVoigt(m_omega, m_eta);
}

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

double Profile1DVoigt::standardizedFT(double q) const
{
    double sum_sq = q * q * m_omega * m_omega;
    return m_eta * std::exp(-sum_sq / 2.0) + (1.0 - m_eta) * 1.0 / (1.0 + sum_sq);
}

double Profile1DVoigt::decayFT(double q) const
{
    double sum_sq = q * q * m_omega * m_omega;
    return m_eta * m_omega * std::sqrt((2 * pi)) * std::exp(-sum_sq / 2.0)
           + (1.0 - m_eta) * m_omega * 2.0 / (1.0 + sum_sq);
}

double Profile1DVoigt::qSecondDerivative() const
{
    return (2.0 - m_eta) * m_omega * m_omega;
}

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