//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Resample/Slice/ProfileUtil.cpp
//! @brief     Implements class ProfileHelper.
//!
//! @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 "Resample/Slice/ProfileUtil.h"
#include "Base/Math/Functions.h"
#include "Base/Util/Assert.h"
#include "Sample/Interface/Roughness.h"
#include "Sample/Material/Material.h"
#include "Sample/Multilayer/Layer.h"
#include "Sample/Multilayer/Slice.h"

namespace {

double Transition(double x, const Roughness* roughness, double rms)
{
    if (!roughness)
        return Math::Heaviside(x);

    return roughness->transient()->transient(x, rms);
}

const std::string SLD = "SLD";
const std::string X = "X";
const std::string Y = "Y";
const std::string Z = "Z";

complex_t quantity(const Material& mat, std::string q)
{
    if (q == SLD)
        return mat.rawData();
    if (q == X)
        return mat.magnetization().x();
    if (q == Y)
        return mat.magnetization().y();
    if (q == Z)
        return mat.magnetization().z();
    ASSERT_NEVER;
}

std::vector<double> vec2real(const std::vector<complex_t>& v)
{
    std::vector<double> result(v.size());
    for (size_t i = 0; i < v.size(); i++)
        result[i] = real(v[i]);
    return result;
}

std::vector<complex_t> profile(const std::vector<std::unique_ptr<const Slice>>& stack,
                               const std::vector<double>& z_values, std::string component)
{
    const complex_t top_value =
        !stack.empty() ? quantity(*stack.at(0)->refractingMaterial(), component) : 0.0;
    std::vector<complex_t> result(z_values.size(), top_value);
    for (size_t i = 1; i < stack.size(); ++i) {
        const Slice& slice = *stack.at(i);
        const Slice& sliceAbove = *stack.at(i - 1);
        const complex_t diff = quantity(*slice.refractingMaterial(), component)
                               - quantity(*sliceAbove.refractingMaterial(), component);
        for (size_t j = 0; j < z_values.size(); ++j) {
            const double arg = (slice.hig() - z_values[j]);
            const double t = Transition(arg, slice.roughness(), slice.rms());
            result[j] += diff * t;
        }
    }
    return result;
}

} // namespace


// Note: for refractive index materials, the material interpolation actually happens at the level
// of n^2. To first order in delta and beta, this implies the same smooth interpolation of delta
// and beta, as is done here.
std::vector<complex_t>
ProfileUtil::sldProfile(const std::vector<std::unique_ptr<const Slice>>& stack,
                        const std::vector<double>& z_values)
{
    return ::profile(stack, z_values, SLD);
}

std::vector<double>
ProfileUtil::magnetizationProfile(const std::vector<std::unique_ptr<const Slice>>& stack,
                                  const std::vector<double>& z_values, std::string xyz)
{
    if (xyz != X && xyz != Y && xyz != Z)
        throw std::runtime_error("Incorrect magnetization component \"" + xyz + "\".\nOnly \"" + X
                                 + "\", \"" + Y + "\" or \"" + Z + "\" are allowed.");

    return ::vec2real(::profile(stack, z_values, xyz));
}

std::pair<double, double>
ProfileUtil::defaultLimits(const std::vector<std::unique_ptr<const Slice>>& stack)
{
    if (stack.size() < 2)
        return {0.0, 0.0};
    double interface_span = stack.front()->low() - stack.back()->hig();
    double default_margin = interface_span > 0.0 ? interface_span / 20.0 : 10.0;
    const double top_rms = stack.at(1)->rms();
    const double bottom_rms = stack.back()->rms();

    double top_margin = top_rms > 0 ? 5.0 * top_rms : default_margin;
    double bottom_margin = bottom_rms > 0 ? 5.0 * bottom_rms : default_margin;
    double z_min = stack.back()->hig() - bottom_margin;
    double z_max = stack.front()->low() + top_margin;
    return {z_min, z_max};
}
