//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Device/Data/Datafield.h
//! @brief     Defines class Datafield.
//!
//! @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)
//
//  ************************************************************************************************

#ifndef BORNAGAIN_DEVICE_DATA_DATAFIELD_H
#define BORNAGAIN_DEVICE_DATA_DATAFIELD_H

#include "Base/Py/ArrayWrapper.h" // Arrayf64Wrapper
#include "Base/Type/Field2D.h"
#include <memory>
#include <string>
#include <vector>

using std::size_t;

class Frame;
class Scale;

//! Stores n-dimensional data with n associated scales.

class Datafield {
public:
    Datafield(const std::string& title, const Frame& frame, const std::vector<double>& values,
              const std::vector<double>& errSigmas = {});
    Datafield(const std::string& title, const Frame& frame);

    //! Constructor that takes ownership of supplied frame and initializes values and errorbars.
    Datafield(const Frame& frame, const std::vector<double>& values,
              const std::vector<double>& errSigmas = {});

    //! Constructor that takes ownership of supplied frame.
    Datafield(const Frame& frame);

    //! Constructor that takes ownership of supplied axes and initializes values and errorbars.
    Datafield(const std::vector<const Scale*>& axes, const std::vector<double>& values,
              const std::vector<double>& errSigmas = {});

    //! Constructor that takes ownership of supplied axes.
    Datafield(const std::vector<const Scale*>& axes);

    //! Constructor that creates equidistant grid from 2D array.
    Datafield(const std::string& xlabel, const std::string& ylabel, const double2d_t& vec);

    Datafield(const Datafield&);

#ifndef SWIG
    Datafield(Datafield&&) noexcept;

    Datafield* clone() const;
#endif

    virtual ~Datafield();

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

    //... for use from Python:

    void setAt(size_t i, double val);
    double valAt(size_t i) const { return m_values[i]; }
#ifdef BORNAGAIN_PYTHON
    //! Returns data to construct a Python Numpy array.
    Arrayf64Wrapper xCenters() const;
    Arrayf64Wrapper dataArray() const;
    Arrayf64Wrapper errors() const;
#endif

    //... retrieve basic info

    const Frame& frame() const;
    size_t rank() const;
    const Scale& axis(size_t k) const;
    const Scale& xAxis() const;
    const Scale& yAxis() const;

    //! Returns total size of data buffer (product of bin number in every dimension).
    size_t size() const;
    bool empty() const { return size() == 0; }

    //! Returns reference to the flat data.
    const std::vector<double>& flatVector() const { return m_values; }

    double maxVal() const;
    double minVal() const;

    //... modifiers

    Datafield plottableField() const;
    Datafield angularField(double lambda, double alpha_i) const;
    Datafield qSpaceField(double lambda, double alpha_i) const;
    Datafield flat() const;
    Datafield noisy(double prefactor, double minimum) const;
    Datafield normalizedToMax() const;

    //! Multiplies contents by constant factor, e.g. to shift curves in log plot.
    void scale(double factor);

    //... helpers

    Datafield* crop(double xmin, double ymin, double xmax, double ymax) const;
    Datafield* crop(double xmin, double xmax) const;

    //! Project a 2D histogram into 1D histogram along X. The projection is made
    //! from all bins along y-axis.
    Datafield* xProjection() const;

    //! @brief Project a 2D histogram into 1D histogram along X. The projection is made
    //! from the y-bin closest to given ordinate yvalue.
    //! @param yvalue the value on y-axis at which projection is taken
    Datafield* xProjection(double yvalue) const;

    //! @brief Project a 2D histogram into 1D histogram along X. The projection is made from
    //! all y-bins corresponding to ordinate between ylow and yup.
    //! @param ylow lower edje on y-axis
    //! @param yup upper edje on y-axis
    Datafield* xProjection(double ylow, double yup) const;

    //! Project a 2D histogram into 1D histogram along Y. The projection is made
    //! from all bins along x-axis.
    Datafield* yProjection() const;

    //! @brief Project a 2D histogram into 1D histogram along Y. The projection is made
    //! from the x-bin closest to given abscissa xvalue.
    //! @param xvalue the value on x-axis at which projection is taken
    Datafield* yProjection(double xvalue) const;

    //! @brief Project a 2D histogram into 1D histogram along Y. The projection is made from
    //! all x-bins corresponding to abscissa between xlow and xup.
    //! @param xlow lower edje on x-axis
    //! @param xup upper edje on x-axis
    Datafield* yProjection(double xlow, double xup) const;

    //! Sets content of output data to specific value.
    void setAllTo(const double& value); // Py: test only; rm when possible

    const std::vector<double>& errorSigmas() const { return m_err_sigmas; }

#ifndef SWIG
    double& operator[](size_t i) { return m_values[i]; }
    const double& operator[](size_t i) const { return m_values[i]; }

    //! Sets new values to raw data vector.
    void setVector(const std::vector<double>& data_vector);
    void setVector2D(const double2d_t& in);

    bool hasErrorSigmas() const;
    std::vector<double>& errorSigmas() { return m_err_sigmas; }

    double2d_t values2D() const;

private:
    std::string m_title;
    std::unique_ptr<const Frame> m_frame;
    std::vector<double> m_values;
    std::vector<double> m_err_sigmas;

    //! Creates projection along X. The projections is made by collecting the data in the range
    //! between [ybinlow, ybinup].
    Datafield* create_xProjection(int ybinlow, int ybinup) const;

    //! Creates projection along Y. The projections is made by collecting the data in the range
    //! between [xbinlow, xbinup].
    Datafield* create_yProjection(int xbinlow, int xbinup) const;

#endif // SWIG
};

#endif // BORNAGAIN_DEVICE_DATA_DATAFIELD_H
