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

CoreAndShell::CoreAndShell(const IParticle& core, const Particle& shell)
    : m_core(core.clone())
    , m_shell(shell.clone())
{
}

CoreAndShell::~CoreAndShell() = default;

CoreAndShell* CoreAndShell::clone() const
{
    auto* result = new CoreAndShell(*m_core, *m_shell);
    instructClone(result);
    return result;
}

void CoreAndShell::validate() const
{
    if (m_validated)
        return;

    ASSERT(m_core);
    ASSERT(m_shell);
    ASSERT(m_shell->pFormfactor());

    if (!dynamic_cast<const Particle*>(m_core.get())
        && !dynamic_cast<const CoreAndShell*>(m_core.get()))
        throw std::runtime_error("CoreAndShell: core must be a Particle or a nested CoreAndShell");

    m_core->validate();
    m_shell->validate();

    if (m_core->topZ() > m_shell->topZ() || m_core->bottomZ() < m_shell->bottomZ())
        throw std::runtime_error("CoreAndShell: core extends beyond shell in z direction");

    const double outer_volume = m_shell->pFormfactor()->volume();
    const double core_volume = m_core->volumeToAdmix();
    if (!Numeric::lessOrAlmostEqual(core_volume, outer_volume, 1e3))
        throw std::runtime_error("CoreAndShell: core volume exceeds shell outer volume");

    m_validated = true;
}

void CoreAndShell::setSliceNumber(size_t n)
{
    IParticle::setSliceNumber(n);
    m_core->setSliceNumber(n);
    m_shell->setSliceNumber(n);
}

double CoreAndShell::bottomZ() const
{
    return m_shell->bottomZ() + particlePosition().z();
}

double CoreAndShell::topZ() const
{
    return m_shell->topZ() + particlePosition().z();
}

double CoreAndShell::volumeToAdmix() const
{
    // Make sure the core volume has been subtracted from the shell beforehand.
    const double combined_volume = m_shell->volumeToAdmix() + m_core->volumeToAdmix();
    const double outer_volume = m_shell->pFormfactor()->volume();
    ASSERT(Numeric::almostEqual(combined_volume, outer_volume, 1e3));
    return outer_volume;
}

std::unique_ptr<const Material> CoreAndShell::materialToAdmix() const
{
    std::vector<OneAdmixture> admixtures;
    admixtures.emplace_back(m_core->volumeToAdmix(), *m_core->materialToAdmix());
    admixtures.emplace_back(m_shell->volumeToAdmix(), *m_shell->materialToAdmix());
    return MixUtil::averagedMaterial("CoreAndShell_avg", admixtures);
}

double CoreAndShell::radius() const
{
    return m_shell->radius();
}

double CoreAndShell::radialExtension() const
{
    return m_shell->radialExtension();
}

bool CoreAndShell::containsSoftParticle() const
{
    return m_core->containsSoftParticle() || m_shell->containsSoftParticle();
}

complex_t CoreAndShell::theFF(const WavevectorInfo& wavevectors) const
{
    return m_shell->theFF(wavevectors) + m_core->theFF(wavevectors);
}

SpinMatrix CoreAndShell::thePolFF(const WavevectorInfo& wavevectors) const
{
    return m_shell->thePolFF(wavevectors) + m_core->thePolFF(wavevectors);
}

ScatteringMoments CoreAndShell::scatteringMoments(const DiffuseElement& ele) const
{
    const auto [m1, v1] = m_shell->scatteringMoments(ele);
    const auto [m2, v2] = m_core->scatteringMoments(ele);
    return {m1 + m2, v1 + v2};
}

PolScatteringMoments CoreAndShell::polScatteringMoments(const DiffuseElement& ele) const
{
    const auto [m1, v1] = m_shell->polScatteringMoments(ele);
    const auto [m2, v2] = m_core->polScatteringMoments(ele);
    return {m1 + m2, v1 + v2};
}

ChannelMoments CoreAndShell::channelMoments(const DwbaChannels& channels) const
{
    const auto shell = m_shell->channelMoments(channels);
    const auto core = m_core->channelMoments(channels);
    ChannelMoments result;
    for (size_t m = 0; m < channels.n_active; ++m) {
        result.mu[m] = shell.mu[m] + core.mu[m];
        for (size_t n = 0; n < channels.n_active; ++n)
            result.C[m][n] = shell.C[m][n] + core.C[m][n];
    }
    return result;
}

PolChannelMoments CoreAndShell::polChannelMoments(const PolDwbaChannels& channels) const
{
    const auto shell = m_shell->polChannelMoments(channels);
    const auto core = m_core->polChannelMoments(channels);
    PolChannelMoments result;
    for (size_t m = 0; m < channels.n_active; ++m) {
        result.mu[m] = shell.mu[m] + core.mu[m];
        for (size_t n = 0; n < channels.n_active; ++n)
            result.C[m][n] = shell.C[m][n] + core.C[m][n];
    }
    return result;
}
