// Copyright (C) 2018 EDF
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#include <Eigen/SVD>
#include <Eigen/Cholesky>
#include "StOpt/regression/LaplacianConstKernelRegression.h"
#include "StOpt/regression/nDDominanceKernel.h"

using namespace std ;
using namespace Eigen ;

namespace StOpt
{

LaplacianConstKernelRegression::LaplacianConstKernelRegression(const bool &p_bZeroDate,
        const ArrayXXd &p_particles,
        const ArrayXd   &p_h):
    BaseRegression(p_bZeroDate, p_particles, false), m_h(p_h), m_tree(p_particles)
{
}
LaplacianConstKernelRegression::LaplacianConstKernelRegression(const ArrayXd   &p_h):
    BaseRegression(false), m_h(p_h)
{
}

LaplacianConstKernelRegression::LaplacianConstKernelRegression(const bool &p_bZeroDate,
        const ArrayXXd  &p_particles):
    BaseRegression(p_bZeroDate, p_particles, false), m_tree(p_particles)
{
}


void LaplacianConstKernelRegression::updateSimulations(const bool &p_bZeroDate, const ArrayXXd &p_particles)
{
    BaseRegression::updateSimulationsBase(p_bZeroDate, p_particles);
    m_tree = KDTree(p_particles);

}

ArrayXXd LaplacianConstKernelRegression::regressFunction(const ArrayXXd &p_fToRegress) const
{
    // dimension
    int nD = m_particles.rows();
    // creation of the 2^d terms
    int nbSum = pow(2, nD);
    vector< shared_ptr<ArrayXXd> > vecToAdd(nbSum);
    // calculate exp values
    Eigen::ArrayXi iCoord(nD) ;
    for (int i = 0; i < nbSum; ++i)
    {
        int ires = i;
        for (int id = nD - 1 ; id >= 0  ; --id)
        {
            unsigned int idec = (ires >> id) ;
            iCoord(id) = -(2 * idec - 1);
            ires -= (idec << id);
        }
        vecToAdd[i] = make_shared<ArrayXXd>(1 + p_fToRegress.rows(), m_particles.cols());
        for (int is = 0; is < m_particles.cols(); ++is)
        {
            double ssum = 0;
            for (int id = 0; id < nD; ++id)
                ssum += iCoord(id) * m_particles(id, is) / m_h(id);
            double expSum = exp(ssum);
            for (int ifunc = 0; ifunc < p_fToRegress.rows(); ++ifunc)
                (*vecToAdd[i])(ifunc, is) = expSum * p_fToRegress(ifunc, is);
            (*vecToAdd[i])(p_fToRegress.rows(), is) = expSum;
        }
    }

    vector< shared_ptr<ArrayXXd> > fDomin(nbSum);


    // kernel resolution
    nDDominanceKernel(m_particles, vecToAdd, fDomin);


    // reconstruction of the 2^d terms
    ArrayXXd reconsY(p_fToRegress.rows(), p_fToRegress.cols());
    ArrayXd recons(p_fToRegress.cols());
    for (int is = 0; is < m_particles.cols(); ++is)
    {
        for (int ifunc = 0; ifunc < p_fToRegress.rows(); ++ifunc)
            reconsY(ifunc, is) =  p_fToRegress(ifunc, is);
        recons(is) = 1;
        for (int id = 0; id < nbSum; ++id)
        {
            for (int ifunc = 0 ; ifunc < p_fToRegress.rows(); ++ifunc)
                reconsY(ifunc, is) += (*fDomin[id])(ifunc, is) * (*vecToAdd[nbSum - 1 - id])(p_fToRegress.rows(), is);
            recons(is) += (*fDomin[id])(p_fToRegress.rows(), is) * (*vecToAdd[nbSum - 1 - id])(p_fToRegress.rows(), is);
        }
    }
    for (int ifunc = 0 ; ifunc < p_fToRegress.rows(); ++ifunc)
        for (int is = 0; is < m_particles.cols(); ++is)
            reconsY(ifunc, is) /= recons(is);
    return reconsY;
}



ArrayXd  LaplacianConstKernelRegression::getCoordBasisFunction(const ArrayXd &p_fToRegress) const
{
    if (!BaseRegression::m_bZeroDate)
    {
        const ArrayXXd  fToRegress = Map<const ArrayXXd>(p_fToRegress.data(), 1, p_fToRegress.size()) ;
        ArrayXXd  regressed = regressFunction(fToRegress);
        ArrayXd toReturn = Map<ArrayXd>(regressed.data(), regressed.size());
        return toReturn;
    }
    else
    {
        ArrayXd retAverage(1);
        retAverage(0) = p_fToRegress.mean();
        return retAverage;
    }
}

ArrayXXd  LaplacianConstKernelRegression::getCoordBasisFunctionMultiple(const ArrayXXd &p_fToRegress) const
{
    if (!BaseRegression::m_bZeroDate)
    {
        return regressFunction(p_fToRegress);
    }
    else
    {
        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;
    }
}

ArrayXd  LaplacianConstKernelRegression::getAllSimulations(const ArrayXd &p_fToRegress) const
{
    if (!BaseRegression::m_bZeroDate)
    {
        const ArrayXXd  fToRegress = Map<const ArrayXXd>(p_fToRegress.data(), 1, p_fToRegress.size()) ;
        ArrayXXd  regressed = regressFunction(fToRegress);
        ArrayXd toReturn = Map<ArrayXd>(regressed.data(), regressed.size());
        return toReturn;
    }
    else
    {
        return ArrayXd::Constant(p_fToRegress.size(), p_fToRegress.mean());
    }
}

ArrayXXd  LaplacianConstKernelRegression::getAllSimulationsMultiple(const ArrayXXd &p_fToRegress) const
{
    if (!BaseRegression::m_bZeroDate)
    {
        return regressFunction(p_fToRegress);
    }
    else
    {
        ArrayXXd ret(p_fToRegress.rows(), p_fToRegress.cols());
        for (int ism = 0; ism < p_fToRegress.rows(); ++ism)
            ret.row(ism).setConstant(p_fToRegress.row(ism).mean());
        return ret;
    }
}


ArrayXd LaplacianConstKernelRegression::reconstruction(const ArrayXd   &p_basisCoefficients) const
{
    if (!BaseRegression::m_bZeroDate)
        return p_basisCoefficients; // basis function are here regressed values !
    else
    {
        return ArrayXd::Constant(m_particles.cols(), p_basisCoefficients(0));
    }
}


ArrayXXd LaplacianConstKernelRegression::reconstructionMultiple(const ArrayXXd   &p_basisCoefficients) const
{
    if (!BaseRegression::m_bZeroDate)
    {
        return p_basisCoefficients; // basis function are here regressed values !
    }
    else
    {
        ArrayXXd retValue(p_basisCoefficients.rows(), m_particles.cols());
        for (int nsm = 0; nsm < p_basisCoefficients.rows(); ++nsm)
            retValue.row(nsm).setConstant(p_basisCoefficients(nsm, 0));
        return retValue ;
    }
}

double  LaplacianConstKernelRegression::reconstructionASim(const int &p_isim, const ArrayXd   &p_basisCoefficients) const
{
    double ret ;
    if (!BaseRegression::m_bZeroDate)
    {
        ret = p_basisCoefficients(p_isim);
    }
    else
    {
        ret = p_basisCoefficients(0);
    }
    return ret ;
}

double LaplacianConstKernelRegression::getValue(const ArrayXd   &p_coordinates,
        const ArrayXd   &p_coordBasisFunction)  const
{
    double ret  ;
    if (!BaseRegression::m_bZeroDate)
    {
        // Use KDTree to find nearest point close to a given one and regress the associated
        // regressed value
        return p_coordBasisFunction(m_tree.nearestIndex(p_coordinates));
    }
    else
        ret =  p_coordBasisFunction(0);
    return ret ;
}


double LaplacianConstKernelRegression::getAValue(const ArrayXd &p_coordinates,  const ArrayXd &p_ptOfStock,
        const vector< shared_ptr<InterpolatorSpectral> > &p_interpFuncBasis) const
{
    if (!BaseRegression::m_bZeroDate)
    {
        return p_interpFuncBasis[m_tree.nearestIndex(p_coordinates)]->apply(p_ptOfStock);
    }
    else
    {
        return p_interpFuncBasis[0]->apply(p_ptOfStock);
    }
}
}
