// Copyright (C) 2016, 2017  EDF
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#ifndef LOCALLINEARREGRESSION_H
#define LOCALLINEARREGRESSION_H
#include <vector>
#include <memory>
#include <array>
#include <Eigen/Dense>
#include <type_traits>
#include <utility>
#include "StOpt/regression/LocalRegression.h"
#include "StOpt/regression/localLinearMatrixOperation.h"
#include "StOpt/core/grids/InterpolatorSpectral.h"

/** \file   LocalLinearRegression.h
 *  \brief  Compute conditional expectations by using local regressions.
 *          See article "Monte-Carlo valorisation of American options: facts and new algorithms to improve existing methods"
 *          by Bouchard, Warin in "Numerical methods in finance", Springer,2012
 *  \author Xavier Warin
 */
namespace StOpt
{
/**
 * \defgroup LocalLinear Piecewise linear regression
 * \brief It implements local linear functions for performing local regression
 * using P-1 finite elements.
 *@{
 */
/// \class LocalLinearRegression LocalLinearRegression.h
/// To be used in Monte Carlo methods. Linear regression on each cell which is constructed such that each cell has
/// roughly the same number of particles
class LocalLinearRegression : public LocalRegression
{
protected :

    Eigen::ArrayXXd m_matReg    ;            ///< Regression matrix (rank  \f$ (n_d+1)^2 \f$ by the number of meshes ). Also store lower part of factorized matrix.
    Eigen::ArrayXXd m_diagReg    ;          ///< Diagonal part of the factorized matrix during Choleski factorization


public :

    /// \brief Default constructor
    LocalLinearRegression() {}

    /// \brief Constructor
    /// \param  p_nbMesh       discretization in each direction
    /// \param  p_bRotationAndRecale do we use SVD
    LocalLinearRegression(const Eigen::ArrayXi   &p_nbMesh, bool  p_bRotationAndRecale = false);

    /// \brief Constructor for object constructed at each time step
    /// \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_nbMesh       discretization in each direction
    /// \param  p_bRotationAndRecale do we use SVD
    LocalLinearRegression(const bool &p_bZeroDate,
                          const Eigen::ArrayXXd   &p_particles,
                          const Eigen::ArrayXi    &p_nbMesh,
                          bool  p_bRotationAndRecale = false);

    /// \brief Second constructor , only to be used in simulation
    /// \param  p_bRotationAndRecale do we use SVD
    LocalLinearRegression(const bool &p_bZeroDate,
                          const   Eigen::ArrayXi &p_nbMesh,
                          const   Eigen::Array< std::array< double, 2>, Eigen::Dynamic, Eigen::Dynamic >   &p_mesh,
                          const std::vector< std::shared_ptr< Eigen::ArrayXd > > &p_mesh1D, const   Eigen::ArrayXd &p_meanX,
                          const   Eigen::ArrayXd   &p_etypX, const   Eigen::MatrixXd   &p_svdMatrix,
                          const bool   &p_bRotationAndRecale) : LocalRegression(p_bZeroDate, p_nbMesh, p_mesh, p_mesh1D, p_meanX,  p_etypX,  p_svdMatrix, p_bRotationAndRecale)
    {
    }

    /// \brief Copy constructor
    /// \param p_objet object to copy
    LocalLinearRegression(const LocalLinearRegression   &p_objet);


    /// \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 Get some local accessors
    ///@{
    inline const Eigen::ArrayXXd &getMatReg() const
    {
        return m_matReg;
    }
    inline const Eigen::ArrayXXd &getDiagReg() const
    {
        return m_diagReg;
    }
    ///@}
    /// \brief conditional expectation basis function coefficient calculation
    /// \param  p_fToRegress  function to regress associated to each simulation used in optimization
    /// \return regression coordinates on the basis  (size : number of meshes multiplied by the dimension plus one)
    /// @{
    Eigen::ArrayXd getCoordBasisFunction(const Eigen::ArrayXd &p_fToRegress) const;
    ///@}
    /// \brief conditional expectation basis function coefficient calculation for multiple functions to regress
    /// \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 regression coordinates on the basis  (size :  number of function to regress  \times number of meshes multiplied by the dimension plus one)
    /// @{
    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 modify cell to fit data and get a convex mapping for a function to regress
    ///        Use "Convex piecewise-linear fitting" by Alessandro Magnani · Stephen P. Boyd, Optim Eng (2009)
    ///        http://web.stanford.edu/~boyd/papers/pdf/cvx_pwl_fit.pdf
    ///        The number of iterations should be small (< 10) as the methodology improves the convexity but doesn't give a full convex function
    /// \param  p_fToRegress  function to regress associated to each simulation used in optimization
    /// \param  p_nbIterMax   maximal number of iterations
    ///         Return the regression values
    Eigen::ArrayXd getAllSimulationsConvex(const Eigen::ArrayXd &p_fToRegress, const int &p_nbIterMax)  ;


    /// \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 conditional expectation reconstruction autodiff version
    /// \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
    template<typename T>
    T getValueAutoDiff(const Eigen::Array<T, Eigen::Dynamic, 1> &p_coordinates,
                       const Eigen::Array<T, Eigen::Dynamic, 1> &p_coordBasisFunction) const
    {
      if ((!m_bZeroDate) && (m_nbMesh.size() != 0))
        {
          // rotation
          Eigen::Array<T, Eigen::Dynamic, 1> x= Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(p_coordinates.size());
          for (int i=0; i < m_svdMatrix.rows(); ++i)
            for (int j=0; j <  m_svdMatrix.cols(); ++j)
              x(i)+=  T(m_svdMatrix(i,j))*(p_coordinates(j) - m_meanX(j)) / m_etypX(j);
          Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic> coordBasisFunction2D(1, p_coordBasisFunction.size());
          coordBasisFunction2D.row(0) = p_coordBasisFunction.transpose();
          return localLinearReconstructionOnePoint(x, m_mesh1D, coordBasisFunction2D)(0);
        }
      else
        {
          return p_coordBasisFunction(0);
        }
    }
  

    /// \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<double>> > &p_interpFuncBasis) const;

    /// \brief permits to reconstruct a function with basis functions coefficients values given on a grid autodiff version
    /// \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)
    template<typename T>
    T getAValueAutoDiff(const Eigen::Array<T, Eigen::Dynamic, 1>  &p_coordinates,
                        const Eigen::Array<T, Eigen::Dynamic, 1> &p_ptOfStock,
                        const std::vector< std::shared_ptr<InterpolatorSpectral<T>> > &p_interpFuncBasis) const
    {
      if ((!m_bZeroDate) && (m_nbMesh.size() != 0))
        {
          // rotation : unroll         
          Eigen::Array<T, Eigen::Dynamic, 1> x= Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(p_coordinates.size());
          for (int i=0; i < m_svdMatrix.rows(); ++i)
            for (int j=0; j <  m_svdMatrix.cols(); ++j)
              x(i)+=  T(m_svdMatrix(i,j))*(p_coordinates(j) - m_meanX(j)) / m_etypX(j);
          return localLinearReconsOnePointSimStock(x, p_ptOfStock, p_interpFuncBasis, m_mesh1D);
        }
      else
        {
          return p_interpFuncBasis[0]->apply(p_ptOfStock);
        }      
    }

    /// \brief get the number of basis functions
    inline int getNumberOfFunction() const
    {
        if (m_bZeroDate)
            return 1;
        else
            return m_nbMesh.prod() * (m_nbMesh.size() + 1);
    }

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

    /// \brief conditional expectation basis function coefficient calculation for a special cell
    /// \param  p_iCell     cell number
    /// \param  p_fToRegress  function to regress associated to each simulation used in optimization and corresponding to the cell
    /// \return regression coordinates on the basis  (size : the dimension of the problem plus one)
    /// @{
    Eigen::ArrayXd getCoordBasisFunctionOneCell(const int &p_iCell, const Eigen::ArrayXd &p_fToRegress) const ;
    Eigen::ArrayXXd getCoordBasisFunctionMultipleOneCell(const int &p_iCell, const Eigen::ArrayXXd &p_fToRegress) const ;
    ///@}

    /// \brief Given a particle and the coordinates of the mesh it belongs to, get back the conditional expectation
    /// \param p_oneParticle  The particle generated
    /// \param p_cell      the cell it belongs to
    /// \param p_foncBasisCoef  function basis on the current cell (the row is the number of reconstruction to achieve, the columns the number of function basis)
    /// \return send back the array of conditional expectations
    Eigen::ArrayXd getValuesOneCell(const Eigen::ArrayXd &p_oneParticle, const int &p_cell, const Eigen::ArrayXXd   &p_foncBasisCoef) const;
};
/**@}*/
}
#endif /*LOCALLINEARREGRESSION_H*/
