//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/HardParticle/FormfactorPolyhedron.cpp
//! @brief     Implements interface FormfactorPolyhedron.
//!
//! @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)
//
//  ************************************************************************************************

//! The mathematics implemented here is described in full detail in a paper
//! by Joachim Wuttke, entitled
//! "Form factor (Fourier shape transform) of polygon and polyhedron."

#include "Sample/HardParticle/FormfactorPolyhedron.h"
#include "Base/Util/Assert.h"
#include "Sample/Particle/PolyhedralUtil.h"
#include "Sample/Particle/Rotations.h"
#include <ff/Face.h>
#include <ff/Prism.h>

FormfactorPolyhedron::FormfactorPolyhedron()
    : HardFormfactor()
{
}

FormfactorPolyhedron::~FormfactorPolyhedron() = default;

//! Called by child classes to set faces and other internal variables.

complex_t FormfactorPolyhedron::formfactorAtBottom(C3 q) const
{
    ASSERT(m_validated);
    // ff library v0.4+ places origin at center of mass; shift to bottom for BornAgain convention
    const double shift = -pimpl->z_bottom();
    return pimpl->formfactor(q) * exp_I(q.z() * shift);
}

double FormfactorPolyhedron::volume() const
{
    ASSERT(m_validated);
    return pimpl->volume();
}

std::shared_ptr<ff::Polyhedron> FormfactorPolyhedron::getPolyhedron() const
{
    if (!pimpl)
        return nullptr;
    if (auto casted = std::dynamic_pointer_cast<ff::Prism>(pimpl))
        return std::shared_ptr<ff::Polyhedron>(casted->asPolyhedron());
    if (auto casted = std::dynamic_pointer_cast<ff::Polyhedron>(pimpl))
        return casted;

    return nullptr;
}

bool FormfactorPolyhedron::contains(const R3& position) const
{
    ASSERT(m_validated);
    // Convexity (required for this half-space test to be exact) is enforced once, at
    // construction, by validate().
    if (m_contains_planes.empty()) {
        const std::shared_ptr<ff::Polyhedron> poly = getPolyhedron();
        // Faces (normals, centers) live in the libformfactor relative frame; a query point in the
        // BornAgain bottom-at-zero convention maps there by (x, y, z + z_bottom) - location().
        m_contains_shift = R3(0, 0, poly->z_bottom()) - poly->location();
        const R3 inner = poly->center_of_mass(); // strictly inside; origin if centrosymmetric
        const bool centrosymmetric = poly->topology().symmetry_Ci;
        for (const ff::Face& face : poly->faces()) {
            R3 normal = face.normal();
            const R3 on_plane = face.center_of_polygon();
            if (normal.dot(inner - on_plane) > 0) // orient outward: inner point on negative side
                normal = -normal;
            normal = normal / normal.mag();
            const double offset = normal.dot(on_plane);
            m_contains_planes.emplace_back(normal, offset);
            if (centrosymmetric) // libformfactor stores only one face of each inversion pair
                m_contains_planes.emplace_back(-normal, offset);
        }
    }
    // Exact test (no tolerance): slicing opens a shared interface by one representable
    // floating-point step to assign each interface lattice node to exactly one slice; any
    // positive tolerance would count it twice.
    const R3 p = position + m_contains_shift;
    for (const auto& [normal, offset] : m_contains_planes)
        if (normal.dot(p) - offset > 0)
            return false;
    return true;
}

double FormfactorPolyhedron::radialExtension() const
{
    ASSERT(m_validated);
    return pimpl->radius();
}

double FormfactorPolyhedron::zBottom(const Rotation* rotation) const
{
    ASSERT(m_validated);
    // ff library v0.4+ places origin at center of mass; shift to bottom for BornAgain convention
    const double shift = -pimpl->z_bottom();
    // For unrotated particles, simply add the shift. For rotated particles, the shift
    // is applied before rotation, so the z-component contribution depends on rotation.
    if (!rotation) {
        ASSERT(PolyhedralUtil::zMin(pimpl->vertices(), nullptr) + shift == 0);
        return 0;
    }
    // Apply shift as R3(0,0,shift), then rotate, then take z-component
    const double rotated_shift = rotation->transformed(R3(0, 0, shift)).z();
    return PolyhedralUtil::zMin(pimpl->vertices(), rotation) + rotated_shift;
}

double FormfactorPolyhedron::zTop(const Rotation* rotation) const
{
    ASSERT(m_validated);
    // ff library v0.4+ places origin at center of mass; shift to bottom for BornAgain convention
    const double shift = -pimpl->z_bottom();
    if (!rotation)
        return PolyhedralUtil::zMax(pimpl->vertices(), nullptr) + shift;
    const double rotated_shift = rotation->transformed(R3(0, 0, shift)).z();
    return PolyhedralUtil::zMax(pimpl->vertices(), rotation) + rotated_shift;
}

void FormfactorPolyhedron::validate() const
{
    ASSERT(pimpl);
    // Reject a non-convex polyhedron: every vertex must lie on one side of each face plane.
    const std::shared_ptr<ff::Polyhedron> poly = getPolyhedron();
    const double tol = 1e-6 * poly->radius();
    for (const ff::Face& face : poly->faces()) {
        const R3 normal = face.normal();
        const double offset = normal.dot(face.center_of_polygon());
        bool any_outside = false;
        bool any_inside = false;
        for (const R3& vertex : poly->vertices()) {
            const double signed_distance = normal.dot(vertex - poly->location()) - offset;
            any_outside = any_outside || signed_distance > tol;
            any_inside = any_inside || signed_distance < -tol;
        }
        ASSERT(!(any_outside && any_inside)); // vertices on both sides of a face => not convex
    }
    m_validated = true;
}

void FormfactorPolyhedron::setPolyhedron(const ff::Topology& topology, double z_bottom,
                                         const std::vector<R3>& vertices) const
{
    pimpl = std::make_unique<ff::Polyhedron>(topology, vertices, R3(0, 0, -z_bottom));
}
