//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Tests/Unit/Sample/ParticlePlacementTest.cpp
//! @brief     Tests placement of rotated particles by Layer::deposit2D and Layer::suspend2D.
//!
//! @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 "Base/Const/Units.h"
#include "Sample/Aggregate/Dilute2D.h"
#include "Sample/Aggregate/Struct2D.h"
#include "Sample/HardParticle/HardParticles.h"
#include "Sample/Material/RefractiveMaterial.h"
#include "Sample/Material/Vacuum.h"
#include "Sample/Multilayer/Layer.h"
#include "Sample/Particle/Particle.h"
#include "Sample/Particle/Rotations.h"
#include "Tests/GTestWrapper/google_test.h"
#include <cmath>
#include <memory>
#include <numbers>
#include <vector>

using std::numbers::pi;

// Layer::deposit2D must place a particle such that its lowest point touches the
// bottom interface of the layer, and Layer::suspend2D such that its highest point
// touches the top interface — also when the particle is rotated. Expected z extensions
// are predicted by pure geometric reasoning, following the formfactor geometries
// documented under hugo/content/ref/sample/particle/ff.

namespace {

//! Tolerance on the anchoring of a particle at an interface. The translate-on-deposit
//! shift is computed from the same bottomZ/topZ that are checked here, so the anchoring
//! must hold to machine precision, independently of how accurately the expected
//! extension of a given shape is known.
constexpr double anchor_tol = 1e-13;

//! Deposits and suspends a rotated particle in a layer, and checks that the z extension
//! anchored at the respective interface lies within [min_extension, max_extension].
//! An interval (instead of a single value) accommodates shapes whose rotated extension
//! is computed from an inscribed tessellation and thus may slightly underestimate the
//! true extension.
void checkPlacement(const Formfactor& ff, const Rotation* rotation, double min_extension,
                    double max_extension, double extension_tol)
{
    const Vacuum vacuum;
    const RefractiveMaterial stuff("stuff", MaterialColor(0.5, 0.5, 0.5), 1e-5, 1e-7);
    Particle particle(stuff, ff);
    if (rotation)
        particle.rotate(*rotation);

    // deposit2D anchors the lowest particle point at the bottom interface (z=0 in struct frame)
    Layer bottom_layer(vacuum);
    bottom_layer.deposit2D(Dilute2D(1e-3, particle));
    ASSERT_EQ(1u, bottom_layer.structsAtBottom().size());
    const IParticle* deposited = bottom_layer.structsAtBottom().front()->particle();
    EXPECT_NEAR(0., deposited->bottomZ(), anchor_tol);
    EXPECT_LE(deposited->topZ(), max_extension + extension_tol);
    EXPECT_GE(deposited->topZ(), min_extension - extension_tol);

    // suspend2D anchors the highest particle point at the top interface (z=0 in struct frame)
    Layer top_layer(vacuum);
    top_layer.suspend2D(Dilute2D(1e-3, particle));
    ASSERT_EQ(1u, top_layer.structsAtTop().size());
    const IParticle* suspended = top_layer.structsAtTop().front()->particle();
    EXPECT_NEAR(0., suspended->topZ(), anchor_tol);
    EXPECT_LE(-suspended->bottomZ(), max_extension + extension_tol);
    EXPECT_GE(-suspended->bottomZ(), min_extension - extension_tol);
}

void checkPlacementExact(const Formfactor& ff, const Rotation* rotation, double extension,
                         double extension_tol)
{
    checkPlacement(ff, rotation, extension, extension, extension_tol);
}

struct ShapeCase {
    const char* name;
    std::shared_ptr<Formfactor> ff;
    double height; //!< geometrically expected z extension of the unrotated shape
    double tol;    //!< absolute tolerance on the expected extension
};

//! Returns one instance of each hard-particle formfactor, with geometric height.
std::vector<ShapeCase> allHardShapes()
{
    const double eps = 1e-10;   // heights that follow exactly from constructor arguments
    const double approx = 1e-4; // heights involving inradius constants of limited precision
    return {
        {"Box", std::make_shared<Box>(10., 6., 4.), 4., eps},
        {"Prism3", std::make_shared<Prism3>(5., 4.), 4., eps},
        {"Prism6", std::make_shared<Prism6>(5., 4.), 4., eps},
        {"PlatonicTetrahedron", std::make_shared<PlatonicTetrahedron>(5.), std::sqrt(2. / 3) * 5.,
         approx},
        {"PlatonicOctahedron", std::make_shared<PlatonicOctahedron>(7.), std::sqrt(2.) * 7.,
         approx},
        {"Dodecahedron", std::make_shared<Dodecahedron>(3.), 2 * 1.11352 * 3., approx},
        {"Icosahedron", std::make_shared<Icosahedron>(7.), 2 * 0.755761 * 7., approx},
        {"Pyramid2", std::make_shared<Pyramid2>(5., 3., 2., 60 * Units::deg), 2., eps},
        {"Pyramid3", std::make_shared<Pyramid3>(5., 2., 75 * Units::deg), 2., eps},
        {"Pyramid4", std::make_shared<Pyramid4>(5., 3., 60 * Units::deg), 3., eps},
        {"Pyramid6", std::make_shared<Pyramid6>(3., 2., 60 * Units::deg), 2., eps},
        {"Bipyramid4", std::make_shared<Bipyramid4>(5., 2., 0.6, 60 * Units::deg), 2. * 1.6, eps},
        {"CantellatedCube", std::make_shared<CantellatedCube>(6., 1.), 6., eps},
        {"TruncatedCube", std::make_shared<TruncatedCube>(6., 1.), 6., eps},
        {"Cone", std::make_shared<Cone>(3., 2., 60 * Units::deg), 2., eps},
        {"Cylinder", std::make_shared<Cylinder>(3., 4.), 4., eps},
        {"EllipsoidalCylinder", std::make_shared<EllipsoidalCylinder>(2., 4., 3.), 3., eps},
        {"Ellipsoid", std::make_shared<Ellipsoid>(2., 3., 4.), 8., eps},
        {"Sphere", std::make_shared<Sphere>(3.), 6., eps},
        {"Spheroid", std::make_shared<Spheroid>(3., 5.), 10., eps},
        {"SphericalSegment", std::make_shared<SphericalSegment>(3., 1., 0.5), 4.5, eps},
        {"SpheroidalSegment", std::make_shared<SpheroidalSegment>(3., 5., 1., 0.5), 8.5, eps},
        {"EllipsoidalSegment", std::make_shared<EllipsoidalSegment>(2., 3., 4., 1., 0.5), 6.5, eps},
        {"HorizontalCylinder", std::make_shared<HorizontalCylinder>(2., 9.), 4., eps},
        {"HorizontalCylinderTruncated", std::make_shared<HorizontalCylinder>(2., 9., 0.5, 0.3), 3.2,
         eps},
        {"LongBoxGauss", std::make_shared<LongBoxGauss>(10., 6., 4.), 4., eps},
        {"LongBoxLorentz", std::make_shared<LongBoxLorentz>(10., 6., 4.), 4., eps},
        {"BarGauss", std::make_shared<BarGauss>(10., 6., 4.), 4., eps},
        {"BarLorentz", std::make_shared<BarLorentz>(10., 6., 4.), 4., eps},
        {"CosineRippleBox", std::make_shared<CosineRippleBox>(10., 4., 2.), 2., eps},
        {"CosineRippleGauss", std::make_shared<CosineRippleGauss>(10., 4., 2.), 2., eps},
        {"CosineRippleLorentz", std::make_shared<CosineRippleLorentz>(10., 4., 2.), 2., eps},
        {"SawtoothRippleBox", std::make_shared<SawtoothRippleBox>(10., 4., 2., 1.), 2., eps},
        {"SawtoothRippleGauss", std::make_shared<SawtoothRippleGauss>(10., 4., 2., 1.), 2., eps},
        {"SawtoothRippleLorentz", std::make_shared<SawtoothRippleLorentz>(10., 4., 2., 1.), 2.,
         eps},
    };
}

} // namespace

//! Verifies that each unrotated hard particle is deposited with its bottom face at the
//! bottom interface and suspended with its top at the top interface, with the full
//! geometric height in between.
TEST(ParticlePlacement, Upright)
{
    for (const auto& c : ::allHardShapes()) {
        SCOPED_TRACE(c.name);
        // Verify the frame convention directly: the shape must occupy [0, height] in its
        // own frame. The anchoring checks below could not detect a shifted frame, since
        // deposit2D compensates any offset that is common to zBottom and zTop.
        EXPECT_NEAR(0., c.ff->zBottom(nullptr), ::anchor_tol);
        EXPECT_NEAR(c.height, c.ff->zTop(nullptr), c.tol);
        ::checkPlacementExact(*c.ff, nullptr, c.height, c.tol);
    }
}

//! Verifies that a rotation about the z axis changes neither anchoring nor extension,
//! for each hard particle.
TEST(ParticlePlacement, RotatedAroundZ)
{
    const RotationZ rot(0.37);
    for (const auto& c : ::allHardShapes()) {
        SCOPED_TRACE(c.name);
        ::checkPlacementExact(*c.ff, &rot, c.height, c.tol);
    }
}

//! Verifies that each hard particle flipped upside down (180° about the x axis) is
//! anchored at the interfaces with unchanged extension.
TEST(ParticlePlacement, UpsideDown)
{
    const RotationX rot(pi);
    for (const auto& c : ::allHardShapes()) {
        SCOPED_TRACE(c.name);
        ::checkPlacementExact(*c.ff, &rot, c.height, c.tol);
    }
}

//! Verifies particles tipped over by 90° about the y axis: the anchored extension
//! must equal the lateral x extension of the unrotated shape.
TEST(ParticlePlacement, TippedOver)
{
    const RotationY rot(pi / 2);
    const double eps = 1e-10;
    ::checkPlacementExact(Box(10., 6., 4.), &rot, 10., eps);
    ::checkPlacementExact(Pyramid2(5., 3., 2., 60 * Units::deg), &rot, 5., eps);
    ::checkPlacementExact(Pyramid4(5., 3., 60 * Units::deg), &rot, 5., eps);
    ::checkPlacementExact(TruncatedCube(6., 1.), &rot, 6., eps);
    ::checkPlacementExact(CantellatedCube(6., 1.), &rot, 6., eps);
    ::checkPlacementExact(Cylinder(3., 4.), &rot, 6., eps);
    ::checkPlacementExact(Cone(3., 2., 60 * Units::deg), &rot, 6., eps);
    ::checkPlacementExact(Sphere(3.), &rot, 6., eps);
    ::checkPlacementExact(Spheroid(3., 5.), &rot, 6., eps);
    ::checkPlacementExact(Ellipsoid(2., 3., 4.), &rot, 4., eps);
    ::checkPlacementExact(EllipsoidalCylinder(2., 4., 3.), &rot, 4., eps);
    ::checkPlacementExact(HorizontalCylinder(2., 9.), &rot, 9., eps);
    ::checkPlacementExact(LongBoxGauss(10., 6., 4.), &rot, 10., eps);
    ::checkPlacementExact(BarGauss(10., 6., 4.), &rot, 10., eps);
    ::checkPlacementExact(CosineRippleBox(10., 4., 2.), &rot, 10., eps);
    ::checkPlacementExact(SawtoothRippleBox(10., 4., 2., 1.), &rot, 10., eps);
    // For tessellated ellipsoid segments the equatorial circle falls between vertex
    // rings, so the computed extension slightly underestimates the diameter.
    ::checkPlacement(SphericalSegment(3., 1., 0.5), &rot, 0.995 * 6., 6., eps);
    ::checkPlacement(SpheroidalSegment(3., 5., 1., 0.5), &rot, 0.995 * 6., 6., eps);
}

//! Verifies particles tilted by a generic angle about the y axis, restricted to shapes
//! whose extremal points are exactly represented in the rotated-extension computation.
TEST(ParticlePlacement, TiltedAboutY)
{
    const double theta = 0.4;
    const RotationY rot(theta);
    const double eps = 1e-10;
    ::checkPlacementExact(Box(10., 6., 4.), &rot, 10 * std::sin(theta) + 4 * std::cos(theta), eps);
    ::checkPlacementExact(Cylinder(3., 4.), &rot, 6 * std::sin(theta) + 4 * std::cos(theta), eps);
    ::checkPlacementExact(Sphere(3.), &rot, 6., eps);
    ::checkPlacementExact(HorizontalCylinder(2., 9.), &rot,
                          9 * std::sin(theta) + 4 * std::cos(theta), eps);
}

//! Verifies particles rotated by generic Euler angles. For shapes with exactly known
//! extremal points the extension is exact; for tessellated round shapes the inscribed
//! 24-gon underestimates the true extension by less than 1 percent.
TEST(ParticlePlacement, EulerRotated)
{
    const double alpha = 0.3, beta = 0.4, gamma = 0.5;
    const RotationEuler rot(alpha, beta, gamma); // ZXZ convention
    const double eps = 1e-10;

    // sphere: analytic extension, exact for any rotation
    ::checkPlacementExact(Sphere(3.), &rot, 6., eps);

    // box: support function L|n_x| + W|n_y| + H|n_z| with n = R^T e_z, exact vertices
    const double n_x = std::sin(beta) * std::sin(gamma);
    const double n_y = std::sin(beta) * std::cos(gamma);
    const double n_z = std::cos(beta);
    ::checkPlacementExact(Box(10., 6., 4.), &rot,
                          10 * std::abs(n_x) + 6 * std::abs(n_y) + 4 * std::abs(n_z), eps);

    // round shapes: true extension depends only on the polar angle beta;
    // the tessellated computation may underestimate it
    const double cylinder_extension = 6 * std::sin(beta) + 4 * std::cos(beta);
    ::checkPlacement(Cylinder(3., 4.), &rot, 0.98 * cylinder_extension, cylinder_extension, eps);
    const double spheroid_extension =
        2 * std::sqrt(std::pow(3 * std::sin(beta), 2) + std::pow(5 * std::cos(beta), 2));
    ::checkPlacement(Spheroid(3., 5.), &rot, 0.98 * spheroid_extension, spheroid_extension, eps);
}
