//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Base/Axis/Scale.cpp
//! @brief     Implements interface Scale.
//!
//! @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 "Base/Axis/Scale.h"
#include "Base/Util/Assert.h"
#include <iomanip>

Scale::Scale(std::string name, const std::vector<Bin1D>& bins)
    : m_name(name)
    , m_bins(bins)
{
    ASSERT(size() > 0);
    for (size_t i = 0; i < size() - 1; ++i) {
        ASSERT(bin(i).upperBound() <= bin(i + 1).lowerBound()); // bins must not overlap
        ASSERT(bin(i) != bin(i + 1));                           // bins must not repeat
    }
    if (isScan())
        for (const Bin1D& b : m_bins)
            ASSERT(!b.binSize()); // bin widths must be all zero or all positive
}

size_t Scale::size() const
{
    return m_bins.size();
}

double Scale::min() const
{
    return m_bins.front().lowerBound();
}

double Scale::max() const
{
    return m_bins.back().upperBound();
}

std::pair<double, double> Scale::bounds() const
{
    return {min(), max()};
}

bool Scale::rangeComprises(double value) const
{
    return value >= min() && value < max();
}

double Scale::span() const
{
    return max() - min();
}

double Scale::center() const
{
    return (max() + min()) / 2;
}

const Bin1D& Scale::bin(size_t i) const
{
    return m_bins.at(i);
}

double Scale::binCenter(size_t i) const
{
    return bin(i).center();
}

std::vector<double> Scale::binCenters() const
{
    std::vector<double> result;
    for (const Bin1D& b : m_bins)
        result.emplace_back(b.center());
    return result;
}

size_t Scale::closestIndex(double value) const
{
    for (size_t i = 0; i < size() - 1; ++i)
        if (value < (bin(i).upperBound() + bin(i + 1).lowerBound()) / 2)
            return i;
    return size() - 1;
}

bool Scale::isEquiDivision() const
{
    const size_t N = size();
    for (size_t i = 0; i < N; ++i) {
        const Bin1D& b = bin(i);
        // exactly replicate the computation of bin bounds in the EquiDivision factory function
        if (b.lowerBound() != (N - i) * (min() / N) + i * (max() / N)
            || b.upperBound() != (N - i - 1) * (min() / N) + (i + 1) * (max() / N))
            return false;
    }
    return true;
}

bool Scale::isScan() const
{
    for (const Bin1D& b : bins())
        if (b.binSize())
            return false;
    return true;
}

Scale Scale::clipped(double lower, double upper) const
{
    std::vector<Bin1D> out_bins;
    for (const Bin1D& b : m_bins)
        if (auto bc = b.clipped_or_nil(lower, upper))
            out_bins.emplace_back(bc.value());
    return {m_name, out_bins};
}

Scale Scale::clipped(std::pair<double, double> bounds) const
{
    return clipped(bounds.first, bounds.second);
}

bool Scale::operator==(const Scale& other) const
{
    return m_name == other.m_name && m_bins == other.m_bins;
}

std::ostream& operator<<(std::ostream& ostr, const Scale& ax)
{
    size_t N = ax.size();
    ASSERT(N > 0);

    ostr << std::setprecision(15);

    if (ax.isScan()) {
        ostr << "ListScan(\"" << ax.axisName() << "\", [";
        for (double v : ax.binCenters())
            ostr << v << ",";
        ostr << "])";
        return ostr;
    }

    if (ax.isEquiDivision()) {
        ostr << "EquiDivision(\"" << ax.axisName() << "\", " << ax.size() << ", " << ax.min()
             << ", " << ax.max() << ")";
        return ostr;
    }

    ostr << "GenericScale(\"" << ax.axisName() << "\", [";
    for (const Bin1D& b : ax.bins())
        ostr << b.lowerBound() << "," << b.upperBound() << ",";
    ostr << "])";
    return ostr;
}
