//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Aggregate/RadialParacrystal.cpp
//! @brief     Implements class RadialParacrystal.
//!
//! @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/Aggregate/RadialParacrystal.h"
#include "Base/Spin/SpinMatrix.h"
#include "Base/Util/Assert.h"
#include "Sample/Correlation/Profiles1D.h"
#include "Sample/Element/DiffuseElement.h"
#include "Sample/Lattice/Lattice2D.h"
#include "Sample/Particle/IParticle.h"
#include <limits>

//! Constructor of interference function of radial paracrystal.
//! @param particle: particle (or Mixture) populating the paracrystal
//! @param peak_distance: average distance to the next neighbor in nanometers
//! @param damping_length: the damping (coherence) length of the paracrystal in nanometers
RadialParacrystal::RadialParacrystal(const IParticle& particle, double peak_distance,
                                     double damping_length)
    : Ordered2D(particle)
    , m_peak_distance(peak_distance)
    , m_damping_length(damping_length)
    , m_kappa(0.0)
    , m_domain_size(0.0)
{
    RadialParacrystal::validate();

    // Radial paracrystal is a simplified angular-averaged version of 2D paracrystal with hexagonal
    // lattice => particle density is computed in the same way.
    double area = HexagonalLattice2D(peak_distance, 0).unitCellArea();
    ASSERT(area > 0);
    m_particle_density = 1. / area;
}

RadialParacrystal::~RadialParacrystal() = default;

void RadialParacrystal::setKappa(double kappa)
{
    m_kappa = kappa;
}
void RadialParacrystal::setDomainSize(double size)
{
    m_domain_size = size;
}

void RadialParacrystal::setProbabilityDistribution(const Profile1D& pdf)
{
    m_pdf.reset(pdf.clone());
}

RadialParacrystal* RadialParacrystal::clone() const
{
    auto* result = new RadialParacrystal(*m_particle, m_peak_distance, m_damping_length);
    instructClone(result);
    if (m_pdf)
        result->setProbabilityDistribution(*m_pdf);
    result->setKappa(m_kappa);
    result->setDomainSize(m_domain_size);
    return result;
}

void RadialParacrystal::validate() const
{
    if (m_peak_distance <= 0)
        throw std::runtime_error("peak_distance <= 0");
    if (m_damping_length < 0)
        throw std::runtime_error("damping_length < 0");
    if (m_kappa < 0)
        throw std::runtime_error("kappa < 0");
    if (m_domain_size < 0)
        throw std::runtime_error("domain_size < 0");

    Struct::validate();
    m_validated = true;
}

complex_t RadialParacrystal::FTPDF(double qpar) const
{
    complex_t phase = exp_I(qpar * m_peak_distance);
    double amplitude = m_pdf->standardizedFT(qpar);
    complex_t result = phase * amplitude;
    if (m_damping_length)
        result *= std::exp(-m_peak_distance / m_damping_length);
    return result;
}

double RadialParacrystal::randomSample(int seed) const
{
    return m_pdf->createSampler()->randomSample(seed);
}

double RadialParacrystal::scalarScattering(const DiffuseElement& ele) const
{
    const double qp = ele.meanQ().magxy();
    const double kqp = m_kappa * qp;
    const auto ssca = particle()->sscaMoments(ele, kqp, particle()->radialExtension());

    const complex_t omega = FTPDF(qp);
    const complex_t mean_ff_product = ssca.shifted_plus * std::conj(ssca.shifted_minus);
    const double iff = 2.0 * (mean_ff_product * omega / (1.0 - ssca.phase_coupling * omega)).real();
    return ssca.intensity + DWfactor(ele.meanQ()) * iff;
}

double RadialParacrystal::polarizedScattering(const DiffuseElement& ele) const
{
    const double qp = ele.meanQ().magxy();
    const double kqp = m_kappa * qp;
    const auto ssca = particle()->polSscaMoments(ele, kqp, particle()->radialExtension());
    const SpinMatrix& polarizer = ele.polarizer();
    const SpinMatrix& analyzer = ele.analyzer();

    const complex_t omega = FTPDF(qp);
    const SpinMatrix iff_matrix = (2.0 * omega / (1.0 - ssca.phase_coupling * omega)) * analyzer
                                  * ssca.shifted_plus * polarizer * ssca.shifted_minus.adjoint();

    // The first term is an analyzed intensity matrix. The interference term below is not; its trace
    // is handled separately and must not be routed through SpinMatrix::intensityTrace().
    return SpinMatrix::intensityTrace(analyzer, ssca.intensity)
           + DWfactor(ele.meanQ()) * std::abs(iff_matrix.trace());
}
double RadialParacrystal::iff_without_dw(const R3& q) const
{
    ASSERT(m_validated);
    ASSERT(m_pdf);
    double result = 0.0;
    double qxr = q.x();
    double qyr = q.y();
    double qpar = std::sqrt(qxr * qxr + qyr * qyr);
    int n = static_cast<int>(std::abs(m_domain_size / m_peak_distance));
    auto nd = static_cast<double>(n);
    complex_t fp = FTPDF(qpar);
    if (n < 1) {
        if (std::abs(1.0 - fp) < 10. * std::numeric_limits<double>::epsilon())
            result = m_pdf->qSecondDerivative() / m_peak_distance / m_peak_distance;
        else
            result = ((1.0 + fp) / (1.0 - fp)).real();
    } else {
        if (std::norm(1.0 - fp) < 10. * std::numeric_limits<double>::epsilon())
            result = nd;
        // for (1-fp)*nd small, take the series expansion to second order in nd*(1-fp)
        else if (std::abs(1.0 - fp) * nd < 2e-4) {
            complex_t intermediate =
                (nd - 1.0) / 2.0 + (nd * nd - 1.0) * (fp - 1.0) / 6.0
                + (nd * nd * nd - 2.0 * nd * nd - nd + 2.0) * (fp - 1.0) * (fp - 1.0) / 24.0;
            result = 1.0 + 2.0 * intermediate.real();
        } else {
            complex_t tmp;
            if (std::abs(fp) == 0.0
                || std::log(std::abs(fp)) * nd < std::log(std::numeric_limits<double>::min())) {
                tmp = 0.0;
            } else {
                tmp = std::pow(fp, n);
            }
            complex_t intermediate =
                fp / (1.0 - fp) - fp * (1.0 - tmp) / nd / (1.0 - fp) / (1.0 - fp);
            result = 1.0 + 2.0 * intermediate.real();
        }
    }
    return result;
}
