// Copyright (C) 2016 EDF
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#ifndef GLOBALREGRESSION_H
#define GLOBALREGRESSION_H
#include "StOpt/regression/MultiVariateBasis.h"
#include "StOpt/regression/BaseRegression.h"
#include <Eigen/Dense>


/** \file GlobalRegression.h
 * \brief Computation conditional expectation using  polynomials
 * \author  Xavier Warin
 */
namespace StOpt
{
/** \defgroup Global polynomial regression with  global polynomials function basis
*@{
*/
template< class ClassFunc1D>
class GlobalRegression : public BaseRegression
{
protected:

    MultiVariateBasis<ClassFunc1D> m_basis ;///< Multivariate basis

public :

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

    ///\brief Default constructor
    /// \param   p_degree     degree of the polynomial approximation
    /// \param   p_dim        dimension of the problem
    /// \param  p_bRotationAndRecale do we use SVD
    GlobalRegression(const int &p_degree, const int &p_dim, bool  p_bRotationAndRecale = false): BaseRegression(p_bRotationAndRecale), m_basis(ComputeDegreeSum(), p_dim, p_degree)
    {  }

    /// \brief Constructor for global 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_degree     degree of the polynomial approximation
    /// \param  p_bRotationAndRecale do we use SVD
    GlobalRegression(const bool &p_bZeroDate,
                     const Eigen::ArrayXXd  &p_particles,
                     const int &p_degree,
                     bool  p_bRotationAndRecale = false):
        BaseRegression(p_bZeroDate, p_particles, p_bRotationAndRecale), m_basis(ComputeDegreeSum(), p_particles.rows(), p_degree)
    {  }

    /// \brief constructor used in simulation
    /// \param  p_bZeroDate    first date is 0?
    /// \param  p_basis        function basis
    /// \param  p_meanX            scaled factor in each direction (average of particles values in each direction)
    /// \param  p_etypX            scaled factor in each direction (standard deviation of particles in each direction)
    /// \param  p_svdMatrix        svd matrix transposed  used to transform particles
    /// \param  p_bRotationAndRecale do we use SVD
    GlobalRegression(const bool &p_bZeroDate, const MultiVariateBasis<ClassFunc1D> &p_basis, const   Eigen::ArrayXd &p_meanX,
                     const   Eigen::ArrayXd   &p_etypX, const   Eigen::MatrixXd   &p_svdMatrix, const bool &p_bRotationAndRecale): BaseRegression(p_bZeroDate, p_meanX,  p_etypX,  p_svdMatrix, p_bRotationAndRecale), m_basis(p_basis) {}


    /// \brief Copy constructor
    /// \param p_object  object to copy
    GlobalRegression(const GlobalRegression &p_object): BaseRegression(p_object), m_basis(p_object.getBasis()) {}

    /// \brief Get back basis
    inline const MultiVariateBasis<ClassFunc1D> &getBasis() const
    {
        return m_basis ;
    }

    /// \brief get the number of basis functions
    inline int getNumberOfFunction() const
    {
        return  m_basis.getNumberOfFunctions() ;
    }

    /// \brief Make the basis use a reduced domain using the mapping
    /// \f$ p_x \in D \mapsto \left(\frac{x_i - center_i}{scale_i}\right)_i \f$
    /// \param p_center center of the domain
    /// \param p_scale width of the domain
    inline void setReduced(const Eigen::ArrayXd &p_center, const Eigen::ArrayXd &p_scale)
    {
        m_basis.setReduced(p_center, p_scale);
    }

    ///  \brief Make the basis use a reduced domain using the mapping
    ///  \f$ p_x \in D \mapsto \left(\frac{x_i - (a_i + b_i)/2}{(b_i - a_i)/2}\right)_i \f$
    ///  The domain [a, b] is mapped to [-1,1]^d
    ///  \param p_lower lower bound a of the domain
    ///  \param p_upper upper bound b of the domain
    inline void setDomain(const Eigen::ArrayXd &p_lower, const Eigen::ArrayXd &p_upper)
    {
        m_basis.setDomain(p_lower, p_upper);
    }

    /// \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)
    {
        BaseRegression::updateSimulationsBase(p_bZeroDate, p_particles);
    }

    /// \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
    {
        if (!m_bZeroDate)
        {
            Eigen::ArrayXd basisCoordinates;
            m_basis.fitLeastSquare(basisCoordinates, m_particles, p_fToRegress);
            return basisCoordinates;
        }
        else
        {
            Eigen::ArrayXd retAverage(1);
            retAverage(0) = p_fToRegress.mean();
            return retAverage;
        }
    }

    ///@}
    /// \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
    {
        if (!m_bZeroDate)
        {
            Eigen::ArrayXXd regFunc(p_fToRegress.rows(), m_basis.getNumberOfFunctions());
            for (int nsm = 0; nsm <  p_fToRegress.rows(); ++nsm)
            {
                Eigen::ArrayXd bLoc = p_fToRegress.row(nsm).transpose();
                Eigen::ArrayXd basisCoordinates;
                m_basis.fitLeastSquare(basisCoordinates, m_particles, bLoc);
                regFunc.row(nsm) = basisCoordinates.transpose();
            }
            return regFunc;
        }
        else
        {
            Eigen::ArrayXXd retAverage(p_fToRegress.rows(), 1);
            for (int nsm = 0; nsm <  p_fToRegress.rows(); ++nsm)
                retAverage.row(nsm).setConstant(p_fToRegress.row(nsm).mean());
            return retAverage;
        }
    }
    ///@}

    /// \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
    {
        if (m_bZeroDate)
        {
            return Eigen::ArrayXd::Constant(p_fToRegress.size(), p_fToRegress.mean());
        }

        Eigen::ArrayXd basisCoordinates;
        m_basis.fitLeastSquare(basisCoordinates, m_particles, p_fToRegress);
        Eigen::ArrayXd regressed(p_fToRegress.size());
        for (int is = 0; is < regressed.size(); ++is)
            regressed(is) = m_basis.eval(m_particles.col(is), basisCoordinates);
        return regressed;
    }

    Eigen::ArrayXXd getAllSimulationsMultiple(const Eigen::ArrayXXd &p_fToRegress) const
    {
        Eigen::ArrayXXd ret(p_fToRegress.rows(), p_fToRegress.cols());
        if (m_bZeroDate)
        {
            for (int ism = 0; ism < p_fToRegress.rows(); ++ism)
                ret.row(ism).setConstant(p_fToRegress.row(ism).mean());
            return ret;
        }
        for (int ism = 0 ; ism < p_fToRegress.rows(); ++ism)
        {
            Eigen::ArrayXd toRegress = p_fToRegress.row(ism).transpose();
            Eigen::ArrayXd basisCoordinates;
            m_basis.fitLeastSquare(basisCoordinates, m_particles, toRegress);
            for (int is = 0 ; is < p_fToRegress.cols(); ++is)
                ret(ism, is) = m_basis.eval(m_particles.col(is), basisCoordinates);
        }
        return ret ;
    }

    ///@}

    /// \brief Use basis functions to reconstruct the solution
    /// \param p_basisCoefficients basis coefficients
    ///@{
    Eigen::ArrayXd reconstruction(const Eigen::ArrayXd   &p_basisCoefficients) const
    {
        if (!m_bZeroDate)
        {
            Eigen::ArrayXd regressed(m_particles.cols());
            for (int is = 0 ; is < m_particles.cols(); ++is)
                regressed(is) = m_basis.eval(m_particles.col(is), p_basisCoefficients);
            return regressed;
        }
        else
            return Eigen::ArrayXd::Constant(m_particles.cols(), p_basisCoefficients(0));
    }

    Eigen::ArrayXXd reconstructionMultiple(const Eigen::ArrayXXd   &p_basisCoefficients) const
    {
        Eigen::ArrayXXd regressed(p_basisCoefficients.rows(), m_particles.cols());
        if (!m_bZeroDate)
        {
            for (int irow = 0 ; irow < p_basisCoefficients.rows(); ++irow)
            {
                Eigen::ArrayXd baseLocal = p_basisCoefficients.row(irow).transpose();
                for (int is = 0; is < m_particles.cols(); ++is)
                    regressed(irow, is) = m_basis.eval(m_particles.col(is), baseLocal);
            }
        }
        else
        {
            for (int nsm = 0; nsm < p_basisCoefficients.rows(); ++nsm)
                regressed.row(nsm).setConstant(p_basisCoefficients(nsm, 0));
        }
        return regressed;
    }

    /// @}

    /// \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
    {
        if (!m_bZeroDate)
        {
            return  m_basis.eval(m_particles.col(p_isim), p_basisCoefficients);
        }
        else
        {
            return p_basisCoefficients(0);
        }
    }

    /// \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

    {
        if (!m_bZeroDate)
        {
            // rotation
            Eigen::VectorXd x = m_svdMatrix * ((p_coordinates - m_meanX) / m_etypX).matrix();
            return m_basis.eval(x.array(), p_coordBasisFunction);
        }
        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> > &p_interpFuncBasis) const
    {
        Eigen::ArrayXd coordBasisFunction(p_interpFuncBasis.size());
        for (int i = 0; i < coordBasisFunction.size(); ++i)
        {
            coordBasisFunction(i) = p_interpFuncBasis[i]->apply(p_ptOfStock);
        }
        return  getValue(p_coordinates, coordBasisFunction);
    }

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

/**@}*/
}
#endif /* GLOBALREGRESSION_H */
