//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Particle/Compound.cpp
//! @brief     Implements class Compound.
//!
//! @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 "Sample/Particle/Compound.h"
#include "Base/Spin/SpinMatrix.h"
#include "Base/Util/Assert.h"
#include "Base/Vector/WavevectorInfo.h"
#include "Sample/Element/DiffuseElement.h"
#include "Sample/Material/Admixtures.h"
#include "Sample/Material/Material.h"
#include "Sample/Particle/DwbaChannels.h"
#include "Sample/Particle/Rotations.h"

Compound::Compound() = default;

Compound::~Compound() = default;

void Compound::addComponent(const IParticle& particle)
{
    m_particles.emplace_back(std::unique_ptr<IParticle>{particle.clone()});
}

void Compound::addComponent(const IParticle& particle, const R3& position)
{
    m_particles.emplace_back(std::unique_ptr<IParticle>(particle.clone()->translate(position)));
}

// Please note, that positions is not const reference here. This is intentional, to
// enable python lists to std::vector conversion
void Compound::addComponents(const IParticle& particle, std::vector<R3> positions)
{
    for (const auto& position : positions)
        addComponent(particle, position);
}

void Compound::addComponent(std::unique_ptr<IParticle> particle)
{
    m_particles.emplace_back(std::move(particle));
}

Compound* Compound::clone() const
{
    auto* result = new Compound();
    for (const auto& p : m_particles)
        result->addComponent(*p);
    instructClone(result);
    return result;
}

std::vector<std::unique_ptr<IParticle>> Compound::decompose() const
{
    std::vector<std::unique_ptr<IParticle>> result;
    const auto* rot = particleRotation();
    const auto& translation = particlePosition();
    for (const auto& particle : m_particles) {
        const auto sublist = particle->decompose();
        for (const auto& subparticle : sublist) {
            std::unique_ptr<IParticle> new_subparticle(subparticle->clone());
            new_subparticle->rotate(rot);
            new_subparticle->translate(translation);
            result.emplace_back(std::move(new_subparticle));
        }
    }
    return result;
}

std::vector<const IParticle*> Compound::components() const
{
    std::vector<const IParticle*> result;
    result.reserve(m_particles.size());
    for (const auto& p : m_particles)
        result.emplace_back(p.get());
    return result;
}

void Compound::setSliceNumber(size_t n)
{
    IParticle::setSliceNumber(n);
    for (auto& particle : m_particles)
        particle->setSliceNumber(n);
}

double Compound::bottomZ() const
{
    double result = m_particles[0]->bottomZ();
    for (size_t i = 1; i < m_particles.size(); ++i)
        result = std::min(result, m_particles[i]->bottomZ());
    return result + particlePosition().z();
}

double Compound::topZ() const
{
    double result = m_particles[0]->topZ();
    for (size_t i = 1; i < m_particles.size(); ++i)
        result = std::max(result, m_particles[i]->topZ());
    return result + particlePosition().z();
}

double Compound::volumeToAdmix() const
{
    double result = 0.0;
    for (const auto& particle : m_particles)
        result += particle->volumeToAdmix();
    return result;
}

std::unique_ptr<const Material> Compound::materialToAdmix() const
{
    std::vector<OneAdmixture> admixtures;
    for (const auto& particle : m_particles)
        admixtures.emplace_back(particle->volumeToAdmix(), *particle->materialToAdmix());
    return MixUtil::averagedMaterial("Compound_avg", admixtures);
}

double Compound::radius() const
{
    if (!m_radius)
        throw std::runtime_error("As we cannot automatically compute the radius of a compound, "
                                 "specify it using function setRadius(radius)");
    return *m_radius;
}

double Compound::radialExtension() const
{
    if (m_radius)
        return *m_radius;
    double result = 0.0;
    for (const auto& particle : m_particles)
        result += particle->radialExtension();
    return result;
}

bool Compound::containsSoftParticle() const
{
    for (const auto& particle : m_particles)
        if (particle->containsSoftParticle())
            return true;
    return false;
}

complex_t Compound::theFF(const WavevectorInfo& wavevectors) const
{
    complex_t result(0.0, 0.0);
    for (const auto& particle : m_particles)
        result += particle->theFF(wavevectors);
    return result;
}

SpinMatrix Compound::thePolFF(const WavevectorInfo& wavevectors) const
{
    SpinMatrix result;
    for (const auto& particle : m_particles)
        result += particle->thePolFF(wavevectors);
    return result;
}

ScatteringMoments Compound::scatteringMoments(const DiffuseElement& ele) const
{
    complex_t mean(0.0, 0.0);
    double variance = 0.0;
    for (const auto& particle : m_particles) {
        const auto [m, v] = particle->scatteringMoments(ele);
        mean += m;
        variance += v;
    }
    return {mean, variance};
}

PolScatteringMoments Compound::polScatteringMoments(const DiffuseElement& ele) const
{
    SpinMatrix mean;
    SpinMatrix variance;
    for (const auto& particle : m_particles) {
        const auto [m, v] = particle->polScatteringMoments(ele);
        mean += m;
        variance += v;
    }
    return {mean, variance};
}

ChannelMoments Compound::channelMoments(const DwbaChannels& channels) const
{
    ChannelMoments result;
    for (const auto& particle : m_particles) {
        const auto child = particle->channelMoments(channels);
        for (size_t m = 0; m < channels.n_active; ++m) {
            result.mu[m] += child.mu[m];
            for (size_t n = 0; n < channels.n_active; ++n)
                result.C[m][n] += child.C[m][n];
        }
    }
    return result;
}

PolChannelMoments Compound::polChannelMoments(const PolDwbaChannels& channels) const
{
    PolChannelMoments result;
    for (const auto& particle : m_particles) {
        const auto child = particle->polChannelMoments(channels);
        for (size_t m = 0; m < channels.n_active; ++m) {
            result.mu[m] += child.mu[m];
            for (size_t n = 0; n < channels.n_active; ++n)
                result.C[m][n] += child.C[m][n];
        }
    }
    return result;
}
