// Copyright (C) 2016 EDF
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#ifndef LEGENDREINTERPOLATOR_H
#define LEGENDREINTERPOLATOR_H
#include <vector>
#include <memory>
#include <array>
#include <functional>
#include <Eigen/Dense>
#include "StOpt/core/grids/Interpolator.h"
#include "StOpt/core/utils/constant.h"
#include "StOpt/core/grids/RegularLegendreGrid.h"

/** \file LegendreInterpolator.h
 *  \brief Defines a legendre interpolator on a full grid
 * \author Xavier Warin
 */
namespace StOpt
{
  //class RegularLegendreGrid;

/// \class LegendreInterpolator LegendreInterpolator.h
/// Legendre interpolation object for full grid
/// Version where the Interpolator is associated  to a point where we interpolate
/// Effective version when a lot of functions are to be interpolated.
template<typename T>
class LegendreInterpolator : public Interpolator<T>
{
private :

    int m_nbWeigth ; ///< storing the number of points involved in representation
    Eigen::Array< std::pair<T, int>, Eigen::Dynamic, 1  > m_weightAndPoints ; ///< For interpolation stores the weights and the points number in the mesh

public :

    /** \brief Default constructor
     */
    LegendreInterpolator() {}

    /** \brief Constructor taking the coordinates
     *  \param p_grid   is the grid used to interpolate
     *  \param p_point  is the coordinate of the points used for interpolation
     */
    LegendreInterpolator(const   RegularLegendreGrid *p_grid, const Eigen::Array<T, Eigen::Dynamic, 1>  &p_point)
      {

        std::shared_ptr< std::array< std::function< T(const T &) >, 11 > > legendre = p_grid->getLegendre<T>();
        std::shared_ptr< std::vector< Eigen::ArrayXXd >  >  fInterpol = p_grid->getFInterpol();
        Eigen::ArrayXi poly = p_grid->getPoly();
        // number of points involved
        m_nbWeigth = (1 + poly).prod();
        m_weightAndPoints.resize(m_nbWeigth);
        // coordinate min
        Eigen::ArrayXi  coordmin = p_grid->lowerPositionCoord(p_point.template cast<double>());
        Eigen::ArrayXd  meshSize = p_grid->getMeshSize(coordmin);
        // get back real coordinates renormalized in [-1,1]
        Eigen::Array<T, Eigen::Dynamic, 1> xCoord(p_point.size());
        Eigen::ArrayXd xCoordMin = p_grid->getCoordinateFromIntCoord(coordmin);
        for (int ip = 0; ip < xCoord.size(); ++ip)
          {
            xCoord(ip) = T(2.)*(p_point(ip) - T(xCoordMin(ip))) / T(meshSize(ip)) - T(1.);
            if (xCoord(ip) > T(1.)) xCoord(ip) = T(1.);
            if (xCoord(ip) < T(-1.)) xCoord(ip) = T(-1.);
          }
        // calculate weights
        Eigen::ArrayXi iCoord(p_point.size()) ; // coordinates in the mesh
        // iterate on all  points on the mesh
        for (int j = 0 ; j < m_nbWeigth ; ++j)
          {
            int jloc = j;
            int nPoint = m_nbWeigth;
            for (int id = p_point.size() - 1 ; id >= 0  ; --id)
              {
                nPoint /= (poly(id) + 1);
                iCoord(id) = jloc / nPoint;
                jloc = jloc % nPoint;
              }
            // now iterates
            //  calculate \f$\sum_i ....\sum_j L_i(\xi_{icoord(0)}) L_i(xCoord) \rho_i \kappa_i ...L_j(\xi_{icoord(nd-1)}) L_j(xCoord(nd-1)) \rho_j (j+0.5) \f$
            T weight = T(0.);
            for (int jj = 0 ; jj < m_nbWeigth ; ++jj)
              {
                int jjloc = jj;
                int nnPoint = m_nbWeigth;
                T weightLocal = T(1.);
                for (int id = p_point.size() - 1 ; id >= 0  ; --id)
                  {
                    nnPoint /= (poly(id) + 1);
                    int iiCoord = jjloc / nnPoint;
                    weightLocal *= (*fInterpol)[id](iiCoord, iCoord(id)) * (*legendre)[iiCoord](xCoord[id]);
                    jjloc = jjloc % nnPoint;
                  }
                weight += weightLocal;
              }
            m_weightAndPoints(j) = std::make_pair(weight, p_grid->intCoordPerDimToGlobal(iCoord + coordmin));
          }
      }

    /**  \brief  interpolate
     *  \param  p_dataValues   Values of the data on the grid
     *  \return interpolated value
     */
    T apply(const Eigen::Ref< const Eigen::Array<T, Eigen::Dynamic, 1> > &p_dataValues) const override
    {
        T retInterp = T(0.);
	T minV = T(StOpt::infty);
	T maxV = T(-StOpt::infty);
        for (int i = 0; i < m_weightAndPoints.size(); ++i)
	  {
	    T valOnGrid = p_dataValues(m_weightAndPoints(i).second);
            retInterp  += m_weightAndPoints(i).first *  valOnGrid;
	    minV= std::min(valOnGrid,minV);
	    maxV= std::max(valOnGrid,maxV);
	  }
        return std::max(std::min(retInterp, maxV),minV);
    }

    /**  \brief  interpolate and use vectorization
    *  \param  p_dataValues   Values of the data on the grid. Interpolation is achieved for all values in the first dimension
    *  \return interpolated value
    */
    Eigen::Array<T, Eigen::Dynamic, 1> applyVec(const Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic> &p_dataValues) const override
    {
        Eigen::Array<T, Eigen::Dynamic, 1> retInterp(Eigen::Array<T, Eigen::Dynamic, 1>::Zero(p_dataValues.rows()));
	Eigen::Array<T, Eigen::Dynamic, 1>  minV =  Eigen::Array<T, Eigen::Dynamic, 1> ::Constant(p_dataValues.rows(),T(StOpt::infty));
	Eigen::Array<T, Eigen::Dynamic, 1>  maxV =  Eigen::Array<T, Eigen::Dynamic, 1> ::Constant(p_dataValues.rows(),T(-StOpt::infty));
	Eigen::Array<T, Eigen::Dynamic, 1> valOnGrid(p_dataValues.rows());
        for (int i = 0; i < m_weightAndPoints.size(); ++i)
	  {
	    valOnGrid = p_dataValues.col(m_weightAndPoints(i).second);
            retInterp  += m_weightAndPoints(i).first *  p_dataValues.col(m_weightAndPoints(i).second);
	    minV = minV.min(valOnGrid);
	    maxV=  maxV.max(valOnGrid);
	  }
	return retInterp.max(minV).min(maxV);
    }


    /** \brief  Same as above but avoids copy for Numpy eigen mapping due to storage conventions
     *  \param  p_dataValues   Values of the data on the grid. Interpolation is achieved for all values in the first dimension
     *  \return interpolated value
     */
    inline Eigen::ArrayXd applyVecPy(Eigen::Ref< Eigen::ArrayXXd, 0, Eigen::Stride<Eigen::Dynamic, Eigen::Dynamic> >  p_dataValues) const override
    {
        Eigen::ArrayXd retInterp(Eigen::ArrayXd::Zero(p_dataValues.rows()));
	Eigen::ArrayXd minV =  Eigen::ArrayXd::Constant(p_dataValues.rows(),StOpt::infty);
	Eigen::ArrayXd maxV =  Eigen::ArrayXd::Constant(p_dataValues.rows(),-StOpt::infty);
	Eigen::ArrayXd valOnGrid(p_dataValues.rows());
        for (int i = 0; i < m_weightAndPoints.size(); ++i)
	  {
	    valOnGrid = p_dataValues.col(m_weightAndPoints(i).second);
            retInterp  += m_weightAndPoints(i).first *  p_dataValues.col(m_weightAndPoints(i).second);
	    minV = minV.min(valOnGrid);
	    maxV=  maxV.max(valOnGrid);
	  }
	return retInterp.max(minV).min(maxV); 
    }
};
}


#endif
