//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Aggregate/Dense2D.cpp
//! @brief     Implements class Dense2D.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2026
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Sample/Aggregate/Dense2D.h"
#include "Base/Math/Bessel.h"
#include "Base/Math/Integrator.h"
#include "Sample/Particle/IParticle.h"
#include <cmath>
#include <numbers>
#include <sstream>

using std::numbers::pi;

namespace {

//! Percus-Yevick hard disk structure factor.
//!
//! M.S. Ripoll & C.F. Tejero (1995) Approximate analytical expression for the direct correlation
//! function of hard discs within the Percus-Yevick equation, Molecular Physics, 85:2, 423-428,
//! DOI: 10.1080/00268979500101211

const double p = 7.0 / 3.0 - 4.0 * std::sqrt(3.0) / pi;

double Czero(double packing)
{
    double numerator = 1.0 + packing + 3.0 * p * packing * packing - p * std::pow(packing, 3);
    double denominator = std::pow(1.0 - packing, 3);
    return -numerator / denominator;
}

double S2(double packing)
{
    double factor = 3.0 * packing * packing / 8.0;
    double numerator = 8.0 * (1.0 - 2.0 * p) + (25.0 - 9.0 * p) * p * packing
                       - (7.0 - 3.0 * p) * p * packing * packing;
    double denominator = 1.0 + packing + 3.0 * p * packing * packing - p * std::pow(packing, 3);
    return factor * numerator / denominator;
}

double W2(double x)
{
    return 2.0 * (std::acos(x) - x * std::sqrt(1.0 - x * x)) / pi;
}

double structureFactorPercusYevick(const R3& q, double radius, double density)
{
    const double qx = q.x();
    const double qy = q.y();
    const double q2r = 2.0 * std::sqrt(qx * qx + qy * qy) * radius;
    const double packing = pi * radius * radius * density;
    const double c_zero = ::Czero(packing);
    const double s2 = ::S2(packing);
    const double c_q = (2 * pi)
                       * RealIntegrator().integrate(
                           [=](double x) -> double {
                               double cx =
                                   c_zero * (1.0 + 4.0 * packing * (::W2(x / 2.0) - 1.0) + s2 * x);
                               return x * cx * Math::Bessel::J0(q2r * x);
                           },
                           0.0, 1.0);
    const double rho = 4.0 * packing / pi;
    return 1.0 / (1.0 - rho * c_q);
}

} // namespace

Dense2D::Dense2D(double area_density, const IParticle& particle)
    : Disordered2D(area_density, particle)
{
}

Dense2D* Dense2D::clone() const
{
    auto* result = new Dense2D(areaNumberDensity(), *particle());
    Struct::instructClone(result);
    return result;
}

void Dense2D::validate() const
{
    Disordered2D::validate();
    const double radius = particle()->radius();
    if (radius <= 0)
        throw std::runtime_error("PY approximation requires particle radius > 0");
    if (areaNumberDensity() < 0)
        throw std::runtime_error("density < 0");
    const double packing = pi * radius * radius * areaNumberDensity();

    // The Ripoll-Tejero PY expression is a fluid approximation. The 0.65 cutoff is not
    // close packing; it is a conservative validity limit from Nishiyama, J. Appl. Cryst.
    // 58 (2025) 1015-1021, DOI:10.1107/S1600576725002407. The paper tabulates explicit
    // random hard-disk S(q) up to eta=0.65 and reports hexagonal features above eta=0.69
    // that are absent from analytical PY.
    constexpr double max_py_fluid_packing = 0.65;
    // Exact 2D hexagonal close-packing fraction for hard disks, used only in diagnostics.
    const double hexagonal_packing = pi / (2.0 * std::sqrt(3.0));

    if (packing > max_py_fluid_packing) {
        std::ostringstream message;
        message << "Dense2D hard-disk packing fraction " << packing
                << " exceeds Percus-Yevick fluid-model limit " << max_py_fluid_packing
                << " (hexagonal close-packing fraction is " << hexagonal_packing
                << "); reduce density or particle radius";
        throw std::runtime_error(message.str());
    }
}

double Dense2D::structureFactor(const R3& q) const
{
    return ::structureFactorPercusYevick(q, particle()->radius(), areaNumberDensity());
}
