//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sim/Scan/PhysicalScan.cpp
//! @brief     Implements interface PhysicalScan.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2023
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Sim/Scan/PhysicalScan.h"
#include "Base/Axis/Scale.h"
#include "Base/Math/Numeric.h"
#include "Device/Beam/Beam.h"
#include "Device/Pol/PolFilter.h"
#include "Param/Distrib/Distributions.h"

PhysicalScan::PhysicalScan(Scale* axis)
    : BeamScan(axis)
{
}

PhysicalScan::~PhysicalScan() = default;

void PhysicalScan::copyPhysicalScan(PhysicalScan* dest) const
{
    copyBeamScan(dest);
    if (m_lambda_distrib)
        dest->setWavelengthDistribution(*m_lambda_distrib);
    if (m_alpha_distrib)
        dest->setGrazingAngleDistribution(*m_alpha_distrib);
    if (m_phi_distrib)
        dest->setAzimuthalAngleDistribution(*m_phi_distrib);
}

std::vector<const INode*> PhysicalScan::nodeChildren() const
{
    std::vector<const INode*> result;
    for (const INode* n : BeamScan::nodeChildren())
        result << n;
    if (m_lambda_distrib)
        result << m_lambda_distrib.get();
    if (m_alpha_distrib)
        result << m_alpha_distrib.get();
    if (m_phi_distrib)
        result << m_phi_distrib.get();
    return result;
}

double PhysicalScan::commonWavelength() const
{
    if (!isCommonWavelength())
        throw std::runtime_error("Wavelength changes during scan. "
                                 "Use 'wavelengthAt(i)' instead.");
    return m_beams.front()->wavelength();
}

double PhysicalScan::wavelengthAt(size_t i) const
{
    return m_beams[i]->wavelength();
}

double PhysicalScan::commonGrazingAngle() const
{
    if (!isCommonGrazingAngle())
        throw std::runtime_error("Grazing angle changes during scan. "
                                 "Use 'grazingAngleAt(i)' instead.");
    return m_beams.front()->alpha_i();
}

void PhysicalScan::setWavelength(double lambda)
{
    for (auto& b : m_beams)
        b->setWavelength(lambda);
}

void PhysicalScan::setWavelengthDistribution(const IDistribution1D& distr)
{
    m_lambda_distrib.reset(distr.clone());
}

double PhysicalScan::grazingAngleAt(size_t i) const
{
    return m_beams[i]->alpha_i();
}

double PhysicalScan::commonAzimuthalAngle() const
{
    if (!isCommonAzimuthalAngle())
        throw std::runtime_error("Azimuthal angle changes during scan. "
                                 "Use 'azimuthalAngleAt(i)' instead.");
    return m_beams.front()->phi_i();
}

double PhysicalScan::azimuthalAngleAt(size_t i) const
{
    return m_beams[i]->phi_i();
}

void PhysicalScan::setGrazingAngle(double alpha)
{
    for (auto& b : m_beams)
        b->setInclination(alpha);
}

void PhysicalScan::setGrazingAngleDistribution(const IDistribution1D& distr)
{
    m_alpha_distrib.reset(distr.clone());
}

void PhysicalScan::setAzimuthalAngle(double phi)
{
    for (auto& b : m_beams)
        b->setAzimuthalAngle(phi);
}

void PhysicalScan::setAzimuthalAngleDistribution(const IDistribution1D& distr)
{
    m_phi_distrib.reset(distr.clone());
}

size_t PhysicalScan::nDistributionSamples() const
{
    size_t alpha_samples = m_alpha_distrib ? m_alpha_distrib->nSamples() : 1;
    size_t lambda_samples = m_lambda_distrib ? m_lambda_distrib->nSamples() : 1;
    size_t phi_samples = m_phi_distrib ? m_phi_distrib->nSamples() : 1;
    return lambda_samples * alpha_samples * phi_samples;
}

bool PhysicalScan::isCommonWavelength() const
{
    const auto ref = m_beams.front()->wavelength();
    for (const auto& b : m_beams)
        if (!Numeric::almostEqual(b->wavelength(), ref, 1))
            return false;
    return true;
}

bool PhysicalScan::isCommonGrazingAngle() const
{
    const auto ref = m_beams.front()->alpha_i();
    for (const auto& b : m_beams)
        if (!Numeric::almostEqual(b->alpha_i(), ref, 1))
            return false;
    return true;
}

bool PhysicalScan::isCommonAzimuthalAngle() const
{
    const auto ref = m_beams.front()->phi_i();
    for (const auto& b : m_beams)
        if (!Numeric::almostEqual(b->phi_i(), ref, 1))
            return false;
    return true;
}
