// Copyright (C) 2020 EDF
// Copyright (C) 2020 CSIRO Data61
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#ifndef LAPLACIANGRIDKERNELREGRESSION_H
#define LAPLACIANGRIDKERNELREGRESSION_H
#include <vector>
#include <memory>
#include <Eigen/Dense>
#include "StOpt/regression/BaseRegression.h"
#include "StOpt/regression/fastLaplacianKDE.h"


/** \fileLaplacianGridKernelRegression.h
 * \brief Computation conditional expectation using the Laplacian kernel
  *   \f$
 *   k(x) = \frac{1}{2^d h^d}   e^{ -\sum_{i=1}^d \frac{|x_i|}{h}}
 *   \f$
 *  and the  fast summation technique of  Langrene, Warin 2020.
 *  A rectilinear grid of points is used to evluate the kernels and a linear interpolation
 *  is used to get back the KDE at the sample points.
 * The estimation can be done with constant regressions or linear regressions :
 * For constant regression :
 * \f$
 *     E[ y/x ]=  \frac{\sum_{j=1}^M  k(x-x^j) y^j}{ \sum_{j=1}^M  k(x-x^j) }
 * \f$
 * For linear regressions :
 *  \f$
 *      E[ y/x ]=  \frac{1}{M}  \sum_{j=1}^M  k(x-x^j) (y^j - a x^j -b)
 * \f$
 * where $(a,b) =  \argmin_{c,d}  \sum_{j=1}^M  k(x-x^j) (y^j - c x^j -d)^2
 * \author  Xavier Warin, Nicolas Langrené
 */
namespace StOpt
{
class LaplacianGridKernelRegression : public BaseRegression
{
private:

    bool m_bZeroDate ;                    ///< Is the regression date zero ?
    Eigen::ArrayXd m_h ; ///< size of bandwidth in each direction
    double   m_coefNbGridPoint ; ///< Multiplicative coefficient for the number of grid points
    std::vector< std::shared_ptr<Eigen::ArrayXd> > m_z ; ///< In each direction, define the grid points coordinates
    int m_nbPtZ ; ///<  Number of point on grid
    bool m_bLinear ; ///< if true it is a linear regression otherwise a constant one.

    /// \brief calculate the rectilinear grid
    void  rectiConst();

    /// \brief Calculate the differents kernel values on the rectilinear grid m_z with a constant approximation
    ///  \return  the coefficients   (nb of function to regress, size of the grid)
    Eigen::ArrayXXd  kernelCoeffCalcConst(const Eigen::ArrayXXd &p_fToRegress) const;

    /// \brief Calculate the differents kernel values on the rectilinear grid m_z with a linear approximation
    ///  \return  the coefficients   (nb of functions to regress, size of the grid)
    Eigen::ArrayXXd  kernelCoeffCalcLinear(const Eigen::ArrayXXd &p_fToRegress) const ;

    /// \brief  Interpolate at sample points
    /// \param p_regOnGrid  regressed values on grids (nb functions by number of z points)
    /// \return regresses values  (nb functions by number of samples)
    Eigen::ArrayXXd  interpolateAtSample(const Eigen::ArrayXXd &p_regOnGrid) const;

    /// \brief regress one functon
    /// \param  p_fToRegress  function to regress associated to each simulation used in optimization (size :  number of function to regress  \times number of grids points  )
    Eigen::ArrayXXd regressFunction(const Eigen::ArrayXXd &p_fToRegress) const ;


    /// \brief interpolate at a point
    /// \param p_coordinates  coordinate of the point where to interpolate
    /// \param p_regOnGrid    regressed value on z grid
    /// \return interpolated value
    double   interpolateAtPoint(const Eigen::ArrayXd   &p_coordinates,  const Eigen::ArrayXd &p_regOnGrid) const ;

public :


    /// \brief Constructor for grid kernel regression
    /// \param  p_bZeroDate          first date is 0?
    /// \param  p_particles          particles used for the meshes.
    ///                              First dimension  : dimension of the problem,
    ///                              second dimension : the  number of particles
    /// \param  p_h                  size of bandwidth in each direction
    /// \param  p_coefNbGridPoint    Multiplicative coefficient for the number of grid points
    /// \param  p_bLinear            True if linear other constant regression
    LaplacianGridKernelRegression(const bool &p_bZeroDate,
                                  const Eigen::ArrayXXd  &p_particles,
                                  const Eigen::ArrayXd   &p_h,
                                  const double   &p_coefNbGridPoint,
                                  const bool &p_bLinear);

    /// \brief Constructor for grid kernel regression
    /// \param  p_bZeroDate          first date is 0?
    /// \param  p_h                  size of bandwidth in each direction
    /// \param p_coefNbGridPoint     Multiplicative coefficient for the number of grid points
    /// \param  p_bLinear            True if linear other constant regression
    /// \param   p_z                  Rectilinear grid where regressed values are calculated
    LaplacianGridKernelRegression(const bool &p_bZeroDate,
                                  const Eigen::ArrayXd   &p_h,
                                  const double   &p_coefNbGridPoint,
                                  const bool &p_bLinear,
                                  const std::vector< std::shared_ptr<Eigen::ArrayXd> >   &p_z);


    /// \brief Constructor for grid kernel regression
    /// \param  p_h                  size of bandwidth in each direction
    /// \param p_coefNbGridPoint     Multiplicative coefficient for the number of grid points
    /// \param  p_bLinear            True if linear other constant regression
    /// \param   p_z                  Rectilinear grid where regressed values are calculated
    LaplacianGridKernelRegression(const Eigen::ArrayXd   &p_h,
                                  const double   &p_coefNbGridPoint,
                                  const bool &p_bLinear);



    ///\brief default  constructor
    LaplacianGridKernelRegression(): BaseRegression(false) {}



    /// \brief update the particles used in regression  and construct the matrices
    /// \param  p_bZeroDate    first date is 0?
    /// \param  p_particles    particles used for the meshes.
    ///                        First dimension  : dimension of the problem,
    ///                        second dimension : the  number of particles
    void updateSimulations(const bool &p_bZeroDate, const Eigen::ArrayXXd  &p_particles);

    /// \brief  For this kernel method get back the regressed values on a deterministic grid with coordinates given by m_z
    /// \param  p_fToRegress  function to regress associated to each simulation used in optimization
    /// \return regressed values on the grid
    /// @{
    Eigen::ArrayXd getCoordBasisFunction(const Eigen::ArrayXd &p_fToRegress) const;
    ///@}

    /// \brief  For this kernel method get back the regressed values on a deterministic grid with coordinates given by m_z
    /// \param  p_fToRegress  function to regress associated to each simulation used in optimization (size : number of functions to regress \times the number of Monte Carlo simulations)
    /// \return regressed values on the grid  (size :  number of function to regress  \times number of grids points  )
    /// @{
    Eigen::ArrayXXd getCoordBasisFunctionMultiple(const Eigen::ArrayXXd &p_fToRegress) const ;
    ///@}

    /// \brief conditional expectation calculation
    /// \param  p_fToRegress  simulations  to regress used in optimization
    /// \return regressed value function
    /// @{
    Eigen::ArrayXd getAllSimulations(const Eigen::ArrayXd &p_fToRegress) const ;
    Eigen::ArrayXXd getAllSimulationsMultiple(const Eigen::ArrayXXd &p_fToRegress) const;
    ///@}


    /// \brief Use basis functions to reconstruct the solution
    /// \param p_basisCoefficients basis coefficients
    ///@{
    Eigen::ArrayXd reconstruction(const Eigen::ArrayXd   &p_basisCoefficients) const;
    Eigen::ArrayXXd reconstructionMultiple(const Eigen::ArrayXXd   &p_basisCoefficients) const;
    /// @}
    /// \brief use basis function to reconstruct a given simulation
    /// \param p_isim               simulation number
    /// \param p_basisCoefficients  basis coefficients to reconstruct a given conditional expectation
    double reconstructionASim(const int &p_isim, const Eigen::ArrayXd   &p_basisCoefficients) const ;

    /// \brief conditional expectation reconstruction
    /// \param  p_coordinates        coordinates to interpolate (uncertainty sample)
    /// \param  p_coordBasisFunction regression coordinates on the basis  (size: number of meshes multiplied by the dimension plus one)
    /// \return regressed value function reconstructed for each simulation
    double getValue(const Eigen::ArrayXd   &p_coordinates,
                    const Eigen::ArrayXd   &p_coordBasisFunction) const ;


    /// \brief permits to reconstruct a function with basis functions coefficients values given on a grid
    /// \param  p_coordinates          coordinates  (uncertainty sample)
    /// \param  p_ptOfStock            grid point
    /// \param  p_interpFuncBasis      spectral interpolator to interpolate the basis functions  coefficients used in regression on the grid (given for each basis function)
    double getAValue(const Eigen::ArrayXd &p_coordinates,  const Eigen::ArrayXd &p_ptOfStock,
                     const std::vector< std::shared_ptr<InterpolatorSpectral> > &p_interpFuncBasis) const;


    /// \brief get the number of basis functions
    inline int getNumberOfFunction() const
    {
        if (m_bZeroDate)
            return 1;
        else
        {
            return m_particles.cols();
        }
    }

    /// \brief Clone the regressor
    virtual std::shared_ptr<BaseRegression> clone() const
    {
        return std::static_pointer_cast<BaseRegression>(std::make_shared<LaplacianGridKernelRegression>(*this));
    }

    /// \brief is it Linear or Constant regression
    bool getBLinear() const
    {
        return m_bLinear;
    }

    /// \brief  get bandwidth
    Eigen::ArrayXd getH() const
    {
        return m_h;
    }

    /// \brief get coefNbGridPoint
    double getCoefNbGridPoint() const
    {
        return m_coefNbGridPoint;
    }

    /// \brief get back grid
    std::vector< std::shared_ptr<Eigen::ArrayXd> > getZ() const
    {
        return m_z;
    }
};
}

#endif /*   LAPLACIANGRIDKERNELREGRESSION_H  */
