//  ************************************************************************************************
//
//  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/LayerInterface.h"
#include "Sample/Interface/LayerRoughness.h"
#include "Sample/Lattice/Lattice3D.h"
#include "Sample/Multilayer/Layer.h"
#include "Sample/Multilayer/MultiLayer.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 defineLayers(const ComponentKeyHandler& objHandler,
                         const MaterialKeyHandler& matHandler)
{
    std::vector<const Layer*> v = objHandler.objectsOfType<Layer>();
    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());
        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*> v = objHandler.objectsOfType<LayerRoughness>();
    if (v.empty())
        return "";
    std::ostringstream result;
    result << std::setprecision(12);
    result << "\n" << indent() << "# Define roughness\n";
    for (const auto* s : v) {
        const std::string& key = objHandler.obj2key(s);
        result << indent() << key << " = ba." << s->pythonConstructor() << "\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::printNm2(s->positionVariance()) << ")\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 defineMultiLayers(const ComponentKeyHandler& objHandler)
{
    std::vector<const MultiLayer*> v = objHandler.objectsOfType<MultiLayer>();
    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.MultiLayer()\n";
        double ccl = s->crossCorrLength();
        if (ccl > 0.0)
            result << indent() << key << ".setCrossCorrLength(" << ccl << ")\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";
        }
        size_t numberOfLayers = s->numberOfLayers();
        if (numberOfLayers) {
            result << indent() << key << ".addLayer(" << objHandler.obj2key(s->layer(0)) << ")\n";

            size_t i_layer = 1;
            while (i_layer != numberOfLayers) {
                const LayerInterface* layerInterface = s->layerInterface(i_layer - 1);
                if (const LayerRoughness* rough = layerInterface->roughness())
                    result << indent() << key << ".addLayerWithTopRoughness("
                           << objHandler.obj2key(s->layer(i_layer)) << ", "
                           << objHandler.obj2key(rough) << ")\n";
                else
                    result << indent() << key << ".addLayer("
                           << objHandler.obj2key(s->layer(i_layer)) << ")\n";
                i_layer++;
            }
        }
        result << "\n" << indent() << "return " << key << "\n";
    }
    return result.str();
}

} // namespace

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

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

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

    objHandler.insertModel("sample", &sample);
    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<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)
        + defineMultiLayers(objHandler)
        + "\n\n";
    // clang-format on
}

SampleToPython::SampleToPython() = default;
