//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Device/Data/Datafield.cpp
//! @brief     Implements class Datafield.cpp.
//!
//! @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 "Device/Data/Datafield.h"
#include "Base/Axis/Bin.h"
#include "Base/Axis/Frame.h"
#include "Base/Axis/MakeScale.h"
#include "Base/Axis/Scale.h"
#include "Base/Util/Assert.h"
#include <algorithm>
#include <random>

#ifdef BORNAGAIN_PYTHON
namespace {

Arrayf64Wrapper arrayExport(const Frame& frame, const std::vector<double>& flatData,
                            const bool copydata)
{
    ASSERT(frame.rank() <= 2);

    const std::size_t rank = frame.rank();
    int dimensions[2] = {0};

    if (flatData.size() > 0)
        for (std::size_t i = 0; i < rank; i++)
            dimensions[i] = frame.axis(i).size();

    // Change the order of axes assignment for proper interpretation of 2-dim arrays on numpy side.
    // Shaping and reshaping numpy array requires `shape=(rows, cols)`:
    //
    // np_array_1 = np.ctypeslib.as_array(ptr, shape=(rows, cols))
    // np_array_2 = np_array_1.reshape(rows, cols)
    //
    // First argument should be num_rows, then num_columns.
    // So we swap to have the same sequence: `shape = (yAxis().size(), xAxis().size())`
    if (rank == 2)
        std::swap(dimensions[0], dimensions[1]);

    return {rank, dimensions, &flatData, copydata};
}

} // namespace

#endif // BORNAGAIN_PYTHON


Datafield::Datafield(std::string title, const Frame& frame, const std::vector<double>& values,
                     const std::vector<double>& errSigmas)
    : m_title(std::move(title))
    , m_frame(frame.clone())
    , m_values(values)
    , m_err_sigmas(errSigmas)
{
    ASSERT(m_frame);
    ASSERT(m_values.size() == m_frame->size());
    ASSERT(m_err_sigmas.empty() || m_err_sigmas.size() == m_values.size());
}

Datafield::Datafield(const std::string& title, const Frame& frame)
    : Datafield(title, frame, std::vector<double>(frame.size(), 0.))
{
}

Datafield::Datafield(const Frame& frame, const std::vector<double>& values,
                     const std::vector<double>& errSigmas)
    : Datafield("", frame, values, errSigmas)
{
}

Datafield::Datafield(const Frame& frame)
    : Datafield("", frame)
{
}


Datafield::Datafield(const std::string& xlabel, const std::string& ylabel, const double2d_t& vec)
{
    const size_t nrows = vec.size();
    const size_t ncols = vec[0].size();
    ASSERT(nrows > 0);
    ASSERT(ncols > 0);

    if (nrows < 2)
        m_frame = std::make_unique<Frame>(EquiDivision(xlabel, ncols, 0.0, (double)ncols));
    else if (ncols < 2)
        m_frame = std::make_unique<Frame>(EquiDivision(ylabel, nrows, 0.0, (double)nrows));
    else
        m_frame = std::make_unique<Frame>(EquiDivision(xlabel, ncols, 0.0, (double)ncols),
                                          EquiDivision(ylabel, nrows, 0.0, (double)nrows));

    setVector2D(vec);
}

Datafield::Datafield(const Datafield& other)
    : Datafield(other.title(), *other.m_frame, other.m_values, other.m_err_sigmas)
{
}

Datafield::Datafield(Datafield&&) noexcept = default;

Datafield* Datafield::clone() const
{
    return new Datafield(title(), frame(), m_values, m_err_sigmas);
}

Datafield::~Datafield() = default;

void Datafield::setAt(size_t i, double val)
{
    m_values[i] = val;
}

void Datafield::setTitle(const std::string& title)
{
    m_title = title;
}

const Frame& Datafield::frame() const
{
    ASSERT(m_frame);
    return *m_frame;
}

size_t Datafield::rank() const
{
    return frame().rank();
}

const Scale& Datafield::axis(size_t k) const
{
    return frame().axis(k);
}

const Scale& Datafield::xAxis() const
{
    return frame().axis(0);
}

const Scale& Datafield::yAxis() const
{
    return frame().axis(1);
}

size_t Datafield::size() const
{
    ASSERT(frame().size() == m_values.size());
    return frame().size();
}

double Datafield::maxVal() const
{
    return *std::max_element(m_values.begin(), m_values.end());
}

double Datafield::minVal() const
{
    return *std::min_element(m_values.begin(), m_values.end());
}

#ifdef BORNAGAIN_PYTHON

Arrayf64Wrapper Datafield::xCenters() const
{
    // NOTE: the array data is produced on the fly;
    // therefore, the ArrayDescriptor must store the data.
    return ::arrayExport(Frame(xAxis().clone()), xAxis().binCenters(),
                         /* copydata= */ true);
}

Arrayf64Wrapper Datafield::yCenters() const
{
    // NOTE: the array data is produced on the fly;
    // therefore, the ArrayDescriptor must store the data.
    return ::arrayExport(Frame(yAxis().clone()), yAxis().binCenters(),
                         /* copydata= */ true);
}

Arrayf64Wrapper Datafield::intensities() const
{
    return ::arrayExport(frame(), flatVector(), /* copydata= */ false);
}

Arrayf64Wrapper Datafield::errors() const
{
    return ::arrayExport(frame(), errorSigmas(), /* copydata= */ false);
}

#endif // BORNAGAIN_PYTHON

Datafield Datafield::plottableField() const
{
    return {title(), frame().plottableFrame(), m_values, m_err_sigmas};
}

Datafield Datafield::flat() const
{
    return {title(), frame().flat(), m_values, m_err_sigmas};
}

Datafield Datafield::noisy(double prefactor, double minimum) const
{
    auto const seed = 123;
    static auto urbg = std::mt19937(seed);

    std::vector<double> outval(size());
    std::vector<double> errval(size());
    for (size_t i = 0; i < size(); ++i) {
        double mean = valAt(i);
        double stdv = prefactor * sqrt(mean);
        auto norm = std::normal_distribution<double>(mean, stdv);
        outval[i] = std::max(norm(urbg), minimum);
        errval[i] = stdv;
    }
    return {title(), frame(), outval, errval};
}

Datafield Datafield::normalizedToMax() const
{
    const double maxval = *std::max_element(m_values.begin(), m_values.end());
    std::vector<double> outval(size());
    std::vector<double> errval(size());
    for (size_t i = 0; i < size(); ++i) {
        outval[i] = m_values[i] / maxval;
        if (hasErrorSigmas())
            errval[i] = m_err_sigmas[i] / maxval;
    }
    return {title(), frame(), outval, errval};
}

Datafield Datafield::crop(double xmin, double ymin, double xmax, double ymax) const
{
    ASSERT(rank() == 2);

    const size_t N = size();
    std::vector<double> out;
    for (size_t i = 0; i < N; ++i) {
        const Bin1D& x_bin = frame().projectedBin(i, 0);
        const Bin1D& y_bin = frame().projectedBin(i, 1);
        if (xmin <= x_bin.max() && x_bin.min() <= xmax && ymin <= y_bin.max()
            && y_bin.min() <= ymax)
            out.push_back(m_values[i]);
    }

    const Scale* xclipped = xAxis().clipped(xmin, xmax).clone();
    const Scale* yclipped = yAxis().clipped(ymin, ymax).clone();
    const Frame outframe = Frame(xclipped, yclipped);

    ASSERT(outframe.size() == out.size());
    return Datafield(outframe, out);
}

Datafield Datafield::crop(double xmin, double xmax) const
{
    ASSERT(rank() == 1);

    const size_t N = size();
    std::vector<double> out;
    std::vector<double> errout;
    for (size_t i = 0; i < N; ++i) {
        const Bin1D& x_bin = frame().projectedBin(i, 0);
        if (xmin <= x_bin.max() && x_bin.min() <= xmax) {
            out.push_back(m_values[i]);
            if (hasErrorSigmas())
                errout.push_back(m_err_sigmas[i]);
        }
    }
    const Scale* xclipped = xAxis().clipped(xmin, xmax).clone();
    const Frame outframe = Frame(xclipped);

    ASSERT(outframe.xAxis().size() == out.size());
    return Datafield(outframe, out, errout);
}

Datafield Datafield::xProjection() const
{
    return xProjectionByBins(0, static_cast<int>(xAxis().size()) - 1);
}

Datafield Datafield::xProjection(double yvalue) const
{
    int ybin_selected = static_cast<int>(yAxis().closestIndex(yvalue));
    return xProjectionByBins(ybin_selected, ybin_selected);
}

Datafield Datafield::xProjection(double ylow, double yup) const
{
    int ybinlow = static_cast<int>(yAxis().closestIndex(ylow));
    int ybinup = static_cast<int>(yAxis().closestIndex(yup));
    return xProjectionByBins(ybinlow, ybinup);
}

Datafield Datafield::yProjection() const
{
    return yProjectionByBins(0, static_cast<int>(xAxis().size()) - 1);
}

Datafield Datafield::yProjection(double xvalue) const
{
    int xbin_selected = static_cast<int>(xAxis().closestIndex(xvalue));
    return yProjectionByBins(xbin_selected, xbin_selected);
}

Datafield Datafield::yProjection(double xlow, double xup) const
{
    int xbinlow = static_cast<int>(xAxis().closestIndex(xlow));
    int xbinup = static_cast<int>(xAxis().closestIndex(xup));
    return yProjectionByBins(xbinlow, xbinup);
}

void Datafield::setAllTo(const double& value)
{
    for (double& v : m_values)
        v = value;
}

void Datafield::setVector(const std::vector<double>& vector)
{
    ASSERT(vector.size() == frame().size());
    m_values = vector;
}

void Datafield::setVector2D(const double2d_t& in)
{
    m_values = FieldUtil::flatten(in);
}

bool Datafield::hasErrorSigmas() const
{
    return !m_err_sigmas.empty();
}

double2d_t Datafield::values2D() const
{
    return FieldUtil::reshapeTo2D(m_values, axis(1).size());
}

Datafield Datafield::xProjectionByBins(int ybinlow, int ybinup) const
{
    std::vector<double> out(xAxis().size());
    for (size_t i = 0; i < size(); ++i) {
        int ybin = static_cast<int>(frame().projectedIndex(i, 1));
        if (ybin >= ybinlow && ybin <= ybinup) {
            double x = frame().projectedCoord(i, 0);
            size_t iout = xAxis().closestIndex(x);
            out[iout] += valAt(i);
        }
    }
    return Datafield(Frame(xAxis().clone()), out);
}

Datafield Datafield::yProjectionByBins(int xbinlow, int xbinup) const
{
    std::vector<double> out(yAxis().size());
    for (size_t i = 0; i < size(); ++i) {
        int xbin = static_cast<int>(frame().projectedIndex(i, 0));
        if (xbin >= xbinlow && xbin <= xbinup) {

            // TODO: duplicates code from xProjectionByBins, move to Frame ?

            double y = frame().projectedCoord(i, 1);
            size_t iout = yAxis().closestIndex(y);
            out[iout] += valAt(i);
        }
    }
    return Datafield(Frame(yAxis().clone()), out);
}
