//  ************************************************************************************************
//
//  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/Distrib/Distributions.h"
#include "Base/Math/Numeric.h"
#include "Device/Beam/Beam.h"
#include "Device/Beam/Footprint.h"
#include "Device/Pol/PolFilter.h"

PhysicalScan::PhysicalScan(const Scale& axis)
    : BeamScan(axis)
{
}

PhysicalScan::~PhysicalScan() = default;

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

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

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

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

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

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

void PhysicalScan::setFootprint(const Footprint* footprint)
{
    for (auto& b : m_beams)
        b->setFootprint(footprint);
}

const Footprint* PhysicalScan::commonFootprint() const
{
    if (!isCommonFootprint())
        throw std::runtime_error("Footprint function changes during scan. "
                                 "Use 'footprintAt(i)' instead.");
    return m_beams.front()->footprint();
}

const Footprint* PhysicalScan::footprintAt(size_t i) const
{
    return m_beams[i]->footprint();
}

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();
}

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();
}

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;
}

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);
}

bool PhysicalScan::isCommonFootprint() const
{
    const auto* ref_fp = m_beams.front()->footprint();
    if (!ref_fp) {
        // footprint is not set
        for (const auto& b : m_beams)
            if (b->footprint())
                return false;
    } else {
        // footprint is set
        double ref = ref_fp->widthRatio();
        for (const auto& b : m_beams) {
            if (!b->footprint())
                return false;
            // TODO check that footprint type is the same? Isn't it too expensive and redundant?
            if (!Numeric::almostEqual(b->footprint()->widthRatio(), ref, 1))
                return false;
        }
    }
    return true;
}

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;
}
