//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sim/Export/SampleToPython.cpp
//! @brief     Implements class SampleToPython.
//!
//! @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 "Sim/Export/SampleToPython.h"
#include "Base/Py/PyFmt.h"
#include "Base/Util/Assert.h"
#include "Base/Vector/RotMatrix.h"
#include "Param/Node/NodeUtil.h"
#include "Sample/Aggregate/Interferences.h"
#include "Sample/Aggregate/ParticleLayout.h"
#include "Sample/Interface/LayerRoughness.h"
#include "Sample/Lattice/Lattice3D.h"
#include "Sample/Multilayer/Layer.h"
#include "Sample/Multilayer/LayerStack.h"
#include "Sample/Multilayer/Sample.h"
#include "Sample/Particle/Compound.h"
#include "Sample/Particle/CoreAndShell.h"
#include "Sample/Particle/Crystal.h"
#include "Sample/Particle/IFormfactor.h"
#include "Sample/Particle/Mesocrystal.h"
#include "Sample/Particle/Particle.h"
#include "Sample/Scattering/Rotations.h"
#include "Sim/Export/ComponentKeyHandler.h"
#include "Sim/Export/MaterialKeyHandler.h"
#include "Sim/Export/PyFmt2.h"
#include <iomanip>
#include <map>
#include <memory>
#include <set>

using Py::Fmt::indent;

static const std::map<int, char> axisChar{{0, 'X'}, {1, 'Y'}, {2, 'Z'}};

namespace {

void setRotationInformation(const IParticle* particle, std::string name, std::ostringstream& result)
{
    if (!particle->rotation())
        return;
    const RotMatrix matrix = particle->rotation()->rotMatrix();
    // Identity matrix?
    if (matrix.isIdentity())
        return;
    // Rotation around coordinate axis?
    for (int iAxis = 0; iAxis < 3; ++iAxis) {
        std::optional<double> angle = matrix.angleAroundCoordAxis(iAxis);
        if (angle) {
            result << indent() << name << "_rotation = ba.Rotation" << axisChar.at(iAxis) << "("
                   << Py::Fmt::printDegrees(angle.value()) << ")\n";
            result << indent() << name << ".rotate(" << name << "_rotation)\n";
            return;
        }
    }
    // Generic rotation.
    auto angles = matrix.zxzEulerAngles();
    result << indent() << name << "_rotation = ba.RotationEuler("
           << Py::Fmt::printDegrees(angles[0]) << ", " << Py::Fmt::printDegrees(angles[1]) << ", "
           << Py::Fmt::printDegrees(angles[2]) << ")\n";
    result << indent() << name << ".rotate(" << name << "_rotation)\n";
}

void setPositionInformation(const IParticle* particle, std::string name, std::ostringstream& result)
{
    R3 pos = particle->particlePosition();
    if (pos == R3())
        return;

    result << indent() << name << "_position = R3(" << Py::Fmt::printNm(pos.x()) << ", "
           << Py::Fmt::printNm(pos.y()) << ", " << Py::Fmt::printNm(pos.z()) << ")\n";
    result << indent() << name << ".translate(" << name << "_position)\n";
}

std::string defineMaterials(const MaterialKeyHandler& matHandler)
{
    static const std::map<MATERIAL_TYPES, std::string> factory_names{
        {MATERIAL_TYPES::RefractiveMaterial, "RefractiveMaterial"},
        {MATERIAL_TYPES::MaterialBySLD, "MaterialBySLD"}};

    const auto& themap = matHandler.materialMap();
    if (themap.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << indent() << "# Define materials\n";
    std::set<std::string> visitedMaterials;
    for (const auto& it : themap) {
        const std::string& key = it.first;
        if (visitedMaterials.find(key) != visitedMaterials.end())
            continue;
        visitedMaterials.insert(key);
        const Material* p_material = it.second;
        const auto factory_name = factory_names.find(p_material->typeID());
        ASSERT(factory_name != factory_names.cend());
        const complex_t& material_data = p_material->refractiveIndex_or_SLD();
        if (p_material->isScalarMaterial()) {
            result << indent() << matHandler.mat2key(p_material) << " = ba." << factory_name->second
                   << "(\"" << p_material->materialName() << "\", "
                   << Py::Fmt::printDouble(material_data.real()) << ", "
                   << Py::Fmt::printDouble(material_data.imag()) << ")\n";
        } else {
            R3 magnetic_field = p_material->magnetization();
            result << indent() << "magnetic_field = R3(" << magnetic_field.x() << ", "
                   << magnetic_field.y() << ", " << magnetic_field.z() << ")\n";
            result << indent() << matHandler.mat2key(p_material) << " = ba." << factory_name->second
                   << "(\"" << p_material->materialName();
            result << "\", " << Py::Fmt::printDouble(material_data.real()) << ", "
                   << Py::Fmt::printDouble(material_data.imag()) << ", "
                   << "magnetic_field)\n";
        }
    }
    return result.str();
}

std::string defineStacks(const ComponentKeyHandler& objHandler)
{
    std::vector<const LayerStack*> v = objHandler.objectsOfType<LayerStack>();
    if (v.empty())
        return "";

    std::ostringstream result;
    result << "\n" << indent() << "# Define periodic stacks\n";
    result << std::setprecision(12);
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.LayerStack(" << s->numberOfPeriods() << ")\n";

        const auto components = s->components();
        for (const auto* c : components) {
            if (const auto& layer = dynamic_cast<const Layer*>(c))
                result << indent() << key << ".addLayer(" << objHandler.obj2key(layer) << ")\n";
            else if (const auto& stack = dynamic_cast<const LayerStack*>(c))
                result << indent() << key << ".addStack(" << objHandler.obj2key(stack) << ")\n";
            else
                ASSERT_NEVER;
        }
        if (s != v.back())
            result << "\n";
    }
    return result.str();
}

std::string defineLayers(const ComponentKeyHandler& objHandler,
                         const MaterialKeyHandler& matHandler)
{
    std::vector<const Layer*> v = objHandler.objectsOfType<Layer>();
    std::vector<const LayerRoughness*> r = objHandler.objectsOfType<LayerRoughness>();
    ASSERT(v.size() == r.size())
    if (v.empty())
        return "";

    std::ostringstream result;
    result << "\n" << indent() << "# Define layers\n";
    result << std::setprecision(12);
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.Layer(" << matHandler.mat2key(s->material());
        if (s->thickness() != 0)
            result << ", " << Py::Fmt::printNm(s->thickness());

        const LayerRoughness* rough = s->roughness();
        if (rough->showInScriptOrGui())
            result << ", " << objHandler.obj2key(rough);

        result << ")\n";
        if (s->numberOfSlices() != 1)
            result << indent() << key << ".setNumberOfSlices(" << s->numberOfSlices() << ")\n";
        for (const auto* layout : s->layouts())
            result << indent() << key << ".addLayout(" << objHandler.obj2key(layout) << ")\n";
    }
    return result.str();
}

std::string defineRoughnesses(const ComponentKeyHandler& objHandler)
{
    std::vector<const LayerRoughness*> roughness = objHandler.objectsOfType<LayerRoughness>();
    std::vector<const AutocorrelationModel*> autocorrelation =
        objHandler.objectsOfType<AutocorrelationModel>();
    std::vector<const InterlayerModel*> interlayer = objHandler.objectsOfType<InterlayerModel>();
    std::vector<const CrosscorrelationModel*> crosscorrelation =
        objHandler.objectsOfType<CrosscorrelationModel>();

    ASSERT(roughness.size() == interlayer.size());
    ASSERT(roughness.size() == autocorrelation.size());
    ASSERT(roughness.size() >= crosscorrelation.size());
    if (roughness.empty())
        return "";

    bool has_roughness_to_export = false;
    for (const auto* r : roughness)
        if (r->showInScriptOrGui()) {
            has_roughness_to_export = true;
            break;
        }
    if (!has_roughness_to_export)
        return "";

    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define roughness\n";

    // define autocorrelations
    for (size_t i = 0; i < roughness.size(); i++) {
        if (!roughness[i]->showInScriptOrGui())
            continue;
        const std::string& key = objHandler.obj2key(autocorrelation[i]);
        result << indent() << key << " = ba." << autocorrelation[i]->className() << "("
               << autocorrelation[i]->pythonArguments() << ")\n";
    }
    result << "\n";
    // define interlayers
    for (size_t i = 0; i < roughness.size(); i++) {
        if (!roughness[i]->showInScriptOrGui())
            continue;
        const std::string& key = objHandler.obj2key(interlayer[i]);
        result << indent() << key << " = ba." << interlayer[i]->className() << "()\n";
    }
    result << "\n";
    // define crosscorrelations
    if (crosscorrelation.size() > 0) {
        for (const auto r : roughness) {
            if (!r->showInScriptOrGui())
                continue;
            if (const auto cc = r->crosscorrelationModel())
                result << indent() << objHandler.obj2key(cc) << " = ba." << cc->className() << "("
                       << cc->pythonArguments() << ")\n";
        }
        result << "\n";
    }
    // define roughnesses
    for (const auto r : roughness) {
        if (!r->showInScriptOrGui())
            continue;
        const std::string& roughness_key = objHandler.obj2key(r);
        const std::string& autocorr_key = objHandler.obj2key(r->autocorrelationModel());
        const std::string& interlayer_key = objHandler.obj2key(r->interlayerModel());
        result << indent() << roughness_key << " = ba." << r->className() << "(" << autocorr_key
               << ", " << interlayer_key;
        if (const auto crosscorrelation = r->crosscorrelationModel())
            result << ", " << objHandler.obj2key(crosscorrelation);
        result << ")\n";
    }
    return result.str();
}

std::string defineFormfactors(const ComponentKeyHandler& objHandler)
{
    std::vector<const IFormfactor*> formfactors = objHandler.objectsOfType<IFormfactor>();
    if (formfactors.empty())
        return "";
    std::ostringstream result;
    result << "\n" << indent() << "# Define form factors\n";
    result << std::setprecision(12);
    for (const auto* s : formfactors) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba." << s->pythonConstructor() << "\n";
    }
    return result.str();
}

std::string defineInterferences(const ComponentKeyHandler& objHandler)
{
    std::vector<const IInterference*> v = objHandler.objectsOfType<IInterference>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define interference functions\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);

        if (dynamic_cast<const InterferenceNone*>(s))
            result << indent() << key << " = ba.InterferenceNone()\n";

        else if (const auto* iff = dynamic_cast<const Interference1DLattice*>(s)) {
            result << indent() << key << " = ba.Interference1DLattice("
                   << Py::Fmt::printNm(iff->length()) << ", " << Py::Fmt::printDegrees(iff->xi())
                   << ")\n";

            const auto* pdf = NodeUtil::OnlyChildOfType<IProfile1D>(*iff);

            if (pdf->decayLength() != 0.0)
                result << indent() << key << "_pdf  = ba." << pdf->pythonConstructor() << "\n"
                       << indent() << key << ".setDecayFunction(" << key << "_pdf)\n";
        } else if (const auto* iff = dynamic_cast<const InterferenceRadialParacrystal*>(s)) {
            result << indent() << key << " = ba.InterferenceRadialParacrystal("
                   << Py::Fmt::printNm(iff->peakDistance()) << ", "
                   << Py::Fmt::printNm(iff->dampingLength()) << ")\n";

            if (iff->kappa() != 0.0)
                result << indent() << key << ".setKappa(" << Py::Fmt::printDouble(iff->kappa())
                       << ")\n";

            if (iff->domainSize() != 0.0)
                result << indent() << key << ".setDomainSize("
                       << Py::Fmt::printDouble(iff->domainSize()) << ")\n";

            const auto* pdf = NodeUtil::OnlyChildOfType<IProfile1D>(*iff);

            if (pdf->omega() != 0.0)
                result << indent() << key << "_pdf  = ba." << pdf->pythonConstructor() << "\n"
                       << indent() << key << ".setProbabilityDistribution(" << key << "_pdf)\n";
        } else if (const auto* iff = dynamic_cast<const Interference2DLattice*>(s)) {
            const auto* lattice = NodeUtil::OnlyChildOfType<Lattice2D>(*iff);

            result << indent() << key << " = ba.Interference2DLattice("
                   << objHandler.obj2key(lattice) << ")\n";

            const auto* pdf = NodeUtil::OnlyChildOfType<IProfile2D>(*iff);

            result << indent() << key << "_pdf  = ba." << pdf->pythonConstructor() << "\n"
                   << indent() << key << ".setDecayFunction(" << key << "_pdf)\n";

            if (iff->integrationOverXi())
                result << indent() << key << ".setIntegrationOverXi(True)\n";
        } else if (const auto* iff = dynamic_cast<const InterferenceFinite2DLattice*>(s)) {
            const auto* lattice = NodeUtil::OnlyChildOfType<Lattice2D>(*iff);

            result << indent() << key << " = ba.InterferenceFinite2DLattice("
                   << objHandler.obj2key(lattice) << ", " << iff->numberUnitCells1() << ", "
                   << iff->numberUnitCells2() << ")\n";

            if (iff->integrationOverXi())
                result << indent() << key << ".setIntegrationOverXi(True)\n";
        } else if (const auto* iff = dynamic_cast<const Interference2DParacrystal*>(s)) {
            const auto* lattice = NodeUtil::OnlyChildOfType<Lattice2D>(*iff);
            std::vector<double> domainSize = iff->domainSizes();

            result << indent() << key << " = ba.Interference2DParacrystal("
                   << objHandler.obj2key(lattice) << ", " << Py::Fmt::printNm(iff->dampingLength())
                   << ", " << Py::Fmt::printNm(domainSize[0]) << ", "
                   << Py::Fmt::printNm(domainSize[1]) << ")\n";

            if (iff->integrationOverXi())
                result << indent() << key << ".setIntegrationOverXi(True)\n";

            const auto pdf_vector = NodeUtil::ChildNodesOfType<IProfile2D>(*iff);
            if (pdf_vector.size() != 2)
                continue;
            const IProfile2D* pdf = pdf_vector[0];

            result << indent() << key << "_pdf_1  = ba." << pdf->pythonConstructor() << "\n";

            pdf = pdf_vector[1];

            result << indent() << key << "_pdf_2  = ba." << pdf->pythonConstructor() << "\n";
            result << indent() << key << ".setProbabilityDistributions(" << key << "_pdf_1, " << key
                   << "_pdf_2)\n";
        } else if (const auto* lattice_hd = dynamic_cast<const InterferenceHardDisk*>(s)) {
            result << indent() << key << " = ba.InterferenceHardDisk("
                   << Py::Fmt::printNm(lattice_hd->radius()) << ", "
                   << Py::Fmt::printDouble(lattice_hd->density()) << ")\n";
        } else
            ASSERT_NEVER;

        if (s->positionVariance() > 0.0) {
            result << indent() << key << ".setPositionVariance("
                   << Py::Fmt::printNm(s->positionVariance(), 2) << ")\n";
        }
    }
    return result.str();
}

std::string defineParticleLayouts(const ComponentKeyHandler& objHandler)
{
    std::vector<const ParticleLayout*> v = objHandler.objectsOfType<ParticleLayout>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define particle layouts\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.ParticleLayout()\n";
        const auto particles = NodeUtil::ChildNodesOfType<IParticle>(*s);
        for (const auto* particle : particles) {
            double abundance = particle->abundance();
            result << indent() << key << ".addParticle(" << objHandler.obj2key(particle) << ", "
                   << Py::Fmt::printDouble(abundance) << ")\n";
        }
        if (const auto* iff = NodeUtil::OnlyChildOfType<IInterference>(*s))
            result << indent() << key << ".setInterference(" << objHandler.obj2key(iff) << ")\n";
        result << indent() << key << ".setTotalParticleSurfaceDensity("
               << s->totalParticleSurfaceDensity() << ")\n";
    }
    return result.str();
}

std::string defineParticles(const ComponentKeyHandler& objHandler,
                            const MaterialKeyHandler& matHandler)
{
    std::vector<const Particle*> v = objHandler.objectsOfType<Particle>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define particles\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        const auto* ff = NodeUtil::OnlyChildOfType<IFormfactor>(*s);
        ASSERT(ff);
        result << indent() << key << " = ba.Particle(" << matHandler.mat2key(s->material()) << ", "
               << objHandler.obj2key(ff) << ")\n";
        setRotationInformation(s, key, result);
        setPositionInformation(s, key, result);
    }
    return result.str();
}

std::string defineCoreShellParticles(const ComponentKeyHandler& objHandler)
{
    std::vector<const CoreAndShell*> v = objHandler.objectsOfType<CoreAndShell>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define core shell particles\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.CoreAndShell(" << objHandler.obj2key(s->coreParticle())
               << ", " << objHandler.obj2key(s->shellParticle()) << ")\n";
        setRotationInformation(s, key, result);
        setPositionInformation(s, key, result);
    }
    return result.str();
}

std::string defineCompounds(const ComponentKeyHandler& objHandler)
{
    std::vector<const Compound*> v = objHandler.objectsOfType<Compound>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define composition of particles at specific positions\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.Compound()\n";
        const auto particle_list = NodeUtil::ChildNodesOfType<IParticle>(*s);
        for (const auto* particle : particle_list)
            result << indent() << key << ".addComponent(" << objHandler.obj2key(particle) << ")\n";
        setRotationInformation(s, key, result);
        setPositionInformation(s, key, result);
    }
    return result.str();
}

std::string defineMesocrystals(const ComponentKeyHandler& objHandler)
{
    std::vector<const Mesocrystal*> v = objHandler.objectsOfType<Mesocrystal>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define mesocrystals\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        const auto* crystal = NodeUtil::OnlyChildOfType<Crystal>(*s);
        const auto* outer_shape = NodeUtil::OnlyChildOfType<IFormfactor>(*s);
        if (!crystal || !outer_shape)
            continue;
        result << indent() << key << " = ba.Mesocrystal(";
        result << objHandler.obj2key(crystal) << ", ";
        result << objHandler.obj2key(outer_shape) << ")\n";
        setRotationInformation(s, key, result);
        setPositionInformation(s, key, result);
    }
    return result.str();
}

std::string defineLattices2D(const ComponentKeyHandler& objHandler)
{
    std::vector<const Lattice2D*> v = objHandler.objectsOfType<Lattice2D>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define 2D lattices\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.BasicLattice2D(\n";
        result << indent() << indent() << Py::Fmt::printNm(s->length1()) << ", "
               << Py::Fmt::printNm(s->length2()) << ", " << Py::Fmt::printDegrees(s->latticeAngle())
               << ", " << Py::Fmt::printDegrees(s->rotationAngle()) << ")\n";
    }
    return result.str();
}

std::string defineLattices3D(const ComponentKeyHandler& objHandler)
{
    std::vector<const Lattice3D*> v = objHandler.objectsOfType<Lattice3D>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define 3D lattices\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        R3 bas_a = s->basisVectorA();
        R3 bas_b = s->basisVectorB();
        R3 bas_c = s->basisVectorC();
        result << indent() << key << " = ba.Lattice3D(\n";
        result << indent() << indent() << "R3(" << Py::Fmt::printNm(bas_a.x()) << ", "
               << Py::Fmt::printNm(bas_a.y()) << ", " << Py::Fmt::printNm(bas_a.z()) << "),\n";
        result << indent() << indent() << "R3(" << Py::Fmt::printNm(bas_b.x()) << ", "
               << Py::Fmt::printNm(bas_b.y()) << ", " << Py::Fmt::printNm(bas_b.z()) << "),\n";
        result << indent() << indent() << "R3(" << Py::Fmt::printNm(bas_c.x()) << ", "
               << Py::Fmt::printNm(bas_c.y()) << ", " << Py::Fmt::printNm(bas_c.z()) << "))\n";
    }
    return result.str();
}

std::string defineCrystals(const ComponentKeyHandler& objHandler)
{
    std::vector<const Crystal*> v = objHandler.objectsOfType<Crystal>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define crystals\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        const auto* lattice = NodeUtil::OnlyChildOfType<Lattice3D>(*s);
        const auto* basis = NodeUtil::OnlyChildOfType<IParticle>(*s);
        if (!lattice || !basis)
            continue;
        result << indent() << key << " = ba.Crystal(";
        result << objHandler.obj2key(basis) << ", ";
        result << objHandler.obj2key(lattice) << ")\n";
    }
    return result.str();
}

std::string defineSamples(const ComponentKeyHandler& objHandler)
{
    std::vector<const Sample*> v = objHandler.objectsOfType<Sample>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    ASSERT(v.size() == 1); // as long as there is exactly one sample, we shall use the singular
    result << "\n" << indent() << "# Define sample\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba.Sample()\n";
        auto external_field = s->externalField();
        if (external_field.mag() > 0.0) {
            std::string field_name = key + "_external_field";
            result << indent() << field_name << " = R3("
                   << Py::Fmt::printScientificDouble(external_field.x()) << ", "
                   << Py::Fmt::printScientificDouble(external_field.y()) << ", "
                   << Py::Fmt::printScientificDouble(external_field.z()) << ")\n";
            result << indent() << key << ".setExternalField(" << field_name << ")\n";
        }
        const auto components = s->outerStack().components();
        for (const auto* c : components) {
            if (const auto& layer = dynamic_cast<const Layer*>(c))
                result << indent() << key << ".addLayer(" << objHandler.obj2key(layer) << ")\n";
            else if (const auto& stack = dynamic_cast<const LayerStack*>(c))
                result << indent() << key << ".addStack(" << objHandler.obj2key(stack) << ")\n";
            else
                ASSERT_NEVER;
        }
        result << "\n" << indent() << "return " << key << "\n";
    }
    return result.str();
}

} // namespace

//  ************************************************************************************************
//  class SampleToPython
//  ************************************************************************************************

std::string SampleToPython::sampleCode(const Sample& sample)
{
    ComponentKeyHandler objHandler;
    MaterialKeyHandler matHandler;

    for (const auto* x : sample.containedMaterials())
        matHandler.insertMaterial(x);

    objHandler.insertModel("sample", &sample);
    for (const auto* x : NodeUtil::AllDescendantsOfType<LayerStack>(sample))
        objHandler.insertModel("stack", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Layer>(sample))
        objHandler.insertModel("layer", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<LayerRoughness>(sample))
        objHandler.insertModel("roughness", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<CrosscorrelationModel>(sample))
        objHandler.insertModel("crosscorrelation", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<InterlayerModel>(sample))
        objHandler.insertModel("interlayer", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<AutocorrelationModel>(sample))
        objHandler.insertModel("autocorrelation", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<ParticleLayout>(sample))
        objHandler.insertModel("layout", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<IFormfactor>(sample))
        objHandler.insertModel("ff", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<IInterference>(sample))
        objHandler.insertModel("iff", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Particle>(sample))
        objHandler.insertModel("particle", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Compound>(sample))
        objHandler.insertModel("compound", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<CoreAndShell>(sample))
        objHandler.insertModel("coreshell", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Mesocrystal>(sample))
        objHandler.insertModel("mesocrystal", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Lattice2D>(sample))
        objHandler.insertModel("lattice", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Lattice3D>(sample))
        objHandler.insertModel("lattice", x);
    for (const auto* x : NodeUtil::AllDescendantsOfType<Crystal>(sample))
        objHandler.insertModel("crystal", x);

    // clang-format off
    return "def get_sample():\n"
        + defineMaterials(matHandler)
        + defineFormfactors(objHandler)
        + defineParticles(objHandler, matHandler)
        + defineCoreShellParticles(objHandler)
        + defineCompounds(objHandler)
        + defineLattices2D(objHandler)
        + defineLattices3D(objHandler)
        + defineCrystals(objHandler)
        + defineMesocrystals(objHandler)
        + defineInterferences(objHandler)
        + defineParticleLayouts(objHandler)
        + defineRoughnesses(objHandler)
        + defineLayers(objHandler, matHandler)
        + defineStacks(objHandler)
        + defineSamples(objHandler)
        + "\n\n";
    // clang-format on
}

SampleToPython::SampleToPython() = default;
