// Copyright (C) 2016 EDF
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#define BOOST_TEST_MODULE testSparseDimensionAdaptive
#define BOOST_TEST_DYN_LINK
#include <tuple>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <boost/test/unit_test.hpp>
#include "StOpt/core/utils/constant.h"
#include "StOpt/core/grids/SparseSpaceGridBound.h"
#include "StOpt/core/grids/SparseGridBoundIterator.h"
#include "StOpt/core/grids/SparseSpaceGridNoBound.h"
#include "StOpt/core/grids/SparseGridNoBoundIterator.h"

using namespace Eigen;
using namespace StOpt;
using namespace std;

#if defined   __linux
#include <fenv.h>
#define enable_abort_on_floating_point_exception() feenableexcept(FE_DIVBYZERO | FE_INVALID)
#endif


/// For Clang < 3.7 (and above ?) to be compatible GCC 5.1 and above
namespace boost
{
namespace unit_test
{
namespace ut_detail
{
std::string normalize_test_case_name(const_string name)
{
    return (name[0] == '&' ? std::string(name.begin() + 1, name.size() - 1) : std::string(name.begin(), name.size()));
}
}
}
}

// define max function : for interpolation adaptation, the criterium is to take max of interpolated values
class functionMaxLevel
{
public :

    double operator()(const SparseSet::const_iterator &p_iterLevel,
                      const ArrayXd &values) const
    {
        double smax = -infty;
        for (SparseLevel::const_iterator iterIndex = p_iterLevel->second.begin(); iterIndex != p_iterLevel->second.end(); ++iterIndex)
        {
            smax = max(smax, fabs(values(iterIndex->second)));
        }
        return smax;
    }

    double operator()(const vector< double> &p_vec) const
    {
        assert(p_vec.size() > 0);
        double smax = p_vec[0];
        for (size_t i = 1; i < p_vec.size(); ++i)
            smax = max(smax, p_vec[i]);
        return smax;
    }
};

// to test generic precision
void testSparseBoundAdaptive2D(int p_level, int p_degree, double p_precision)
{
    ArrayXd  lowValues(2);
    lowValues(0) = -1;
    lowValues(1) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(2, 2.);
    ArrayXd  weight =   ArrayXd::Constant(2, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return p_x(0) * p_x(0) + p_x(0) * p_x(0);
        else
            return - p_x(0) * exp(p_x(1)) ;
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();
    function< double(const vector< double> &)> fErrorLevels = functionMaxLevel();
    //  adaptation
    sparseGrid.refine(p_precision, fInterpol, fErrorOneLevel, fErrorLevels, valuesFunction, hierarValues) ;


    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(2);
    int bptDim = 100;
    double dSize = sizeDomain(0) / bptDim;
    for (int j = 0; j <= bptDim; ++j)
        for (int i = 0; i <= bptDim; ++i)
        {
            pointCoord(0) = lowValues(0) + i * dSize;
            pointCoord(1) = lowValues(1) + j * dSize;
            double fValue = fInterpol(pointCoord);
            shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
            double interpValue = interpolator->apply(hierarValues);
            double error = fabs(fValue - interpValue);
            errMax = max(errMax, error);
        }
    BOOST_CHECK(errMax < p_precision);
}


BOOST_AUTO_TEST_CASE(testSparseAdaptive2DBound)
{
    {
        int level = 3;
        int degree = 1 ;
        double precision = 1e-5;
        testSparseBoundAdaptive2D(level, degree, precision);
    }
    {
        int level = 3;
        int degree = 2 ;
        double precision = 1e-5;
        testSparseBoundAdaptive2D(level, degree, precision);
    }
    {
        int level = 3;
        int degree = 3 ;
        double precision = 1e-5;
        testSparseBoundAdaptive2D(level, degree, precision);
    }
}


void testSparseBoundAdaptiveCoarsen2D(int p_level, int p_degree, double p_precision)
{
    ArrayXd  lowValues(2);
    lowValues(0) = -1;
    lowValues(1) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(2, 2.);
    ArrayXd  weight =   ArrayXd::Constant(2, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return p_x(0) * p_x(0) + p_x(0) * p_x(0);
        else
            return - p_x(0) * exp(p_x(1)) ;
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();

    // coarsen the grid
    sparseGrid.coarsen(p_precision,  fErrorOneLevel, valuesFunction, hierarValues) ;


    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(2);
    int bptDim = 100;
    double dSize = sizeDomain(0) / bptDim;
    for (int j = 0; j <= bptDim; ++j)
        for (int i = 0; i <= bptDim; ++i)
        {
            pointCoord(0) = lowValues(0) + i * dSize;
            pointCoord(1) = lowValues(1) + j * dSize;
            double fValue = fInterpol(pointCoord);
            shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
            double interpValue = interpolator->apply(hierarValues);
            double error = fabs(fValue - interpValue);
            errMax = max(errMax, error);
        }
    BOOST_CHECK(errMax < p_precision);
}

BOOST_AUTO_TEST_CASE(testSparseAdaptiveCoarsen2DBound)
{
    int level = 7;
    int degree = 1 ;
    // to define which level to erase from initial
    double precision = 1e-3;
    testSparseBoundAdaptiveCoarsen2D(level, degree, precision);

}

void testSparseBoundAdaptiveCoarsenRefine2D(int p_level, int p_degree,  double p_precisionCoarsen, double p_precisionRefine)
{
    ArrayXd  lowValues(2);
    lowValues(0) = -1;
    lowValues(1) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(2, 2.);
    ArrayXd  weight =   ArrayXd::Constant(2, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return p_x(0) * p_x(0) + p_x(0) * p_x(0);
        else
            return - p_x(0) * exp(p_x(1)) ;
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();
    function< double(const vector< double> &)> fErrorLevels = functionMaxLevel();

    // coarsen the grid
    sparseGrid.coarsen(p_precisionCoarsen,  fErrorOneLevel, valuesFunction, hierarValues) ;

    // refine the grid
    sparseGrid.refine(p_precisionRefine, fInterpol, fErrorOneLevel, fErrorLevels, valuesFunction, hierarValues) ;

    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(2);
    int bptDim = 100;
    double dSize = sizeDomain(0) / bptDim;
    for (int j = 0; j <= bptDim; ++j)
        for (int i = 0; i <= bptDim; ++i)
        {
            pointCoord(0) = lowValues(0) + i * dSize;
            pointCoord(1) = lowValues(1) + j * dSize;
            double fValue = fInterpol(pointCoord);
            shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
            double interpValue = interpolator->apply(hierarValues);
            double error = fabs(fValue - interpValue);
            errMax = max(errMax, error);
        }
    BOOST_CHECK(errMax < p_precisionRefine);
}

BOOST_AUTO_TEST_CASE(testSparseAdaptiveCoarsenRefine2DBound)
{
    int level = 4;
    int degree = 1 ;
    // to define which level to erase from initial
    double precisionCoarsen = 1e-2;
    double precisionRefine = 1e-2;
    testSparseBoundAdaptiveCoarsenRefine2D(level, degree, precisionCoarsen,  precisionRefine);

}

// to test generic precision
void testSparseBoundAdaptive3D(int p_level, int p_degree, double p_precision)
{
    ArrayXd  lowValues(3);
    lowValues(0) = -1;
    lowValues(1) = 0;
    lowValues(2) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(3, 2.);
    ArrayXd  weight =   ArrayXd::Constant(3, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return (p_x(0) * p_x(0) + p_x(0) * p_x(0)) * exp(p_x(2));
        else
            return - p_x(0) * exp(p_x(1)) * exp(p_x(2));
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();
    function< double(const vector< double> &)> fErrorLevels = functionMaxLevel();
    //  adaptation
    sparseGrid.refine(p_precision, fInterpol, fErrorOneLevel, fErrorLevels, valuesFunction, hierarValues) ;

    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(3);
    int bptDim = 20;
    double dSize = sizeDomain(0) / bptDim;
    for (int k = 0; k <= bptDim; ++k)
        for (int j = 0; j <= bptDim; ++j)
            for (int i = 0; i <= bptDim; ++i)
            {
                pointCoord(0) = lowValues(0) + i * dSize;
                pointCoord(1) = lowValues(1) + j * dSize;
                pointCoord(2) = lowValues(2) + k * dSize;

                double fValue = fInterpol(pointCoord);
                shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
                double interpValue = interpolator->apply(hierarValues);
                double error = fabs(fValue - interpValue);
                errMax = max(errMax, error);
            }
    BOOST_CHECK(errMax < p_precision);
}



BOOST_AUTO_TEST_CASE(testSparseAdaptive3DBound)
{
    {
        int level = 1;
        int degree = 1 ;
        double precision = 1e-2;
        testSparseBoundAdaptive3D(level, degree, precision);
    }
    {
        int level = 3;
        int degree = 2 ;
        double precision = 1e-2;
        testSparseBoundAdaptive3D(level, degree, precision);
    }
    {
        int level = 3;
        int degree = 3 ;
        double precision = 1e-2;
        testSparseBoundAdaptive3D(level, degree, precision);
    }
}


// to test generic precision
void testSparseNoBoundAdaptive2D(int p_level, int p_degree, double p_precision)
{
    ArrayXd  lowValues(2);
    lowValues(0) = -1;
    lowValues(1) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(2, 2.);
    ArrayXd  weight =   ArrayXd::Constant(2, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return p_x(0) * p_x(0) + p_x(0) * p_x(0);
        else
            return - p_x(0) * exp(p_x(1)) ;
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridNoBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();
    function< double(const vector< double> &)> fErrorLevels = functionMaxLevel();
    //  adaptation
    sparseGrid.refine(p_precision, fInterpol, fErrorOneLevel, fErrorLevels, valuesFunction, hierarValues) ;


    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(2);
    int bptDim = 100;
    double dSize = sizeDomain(0) / bptDim;
    for (int j = 0; j <= bptDim; ++j)
        for (int i = 0; i <= bptDim; ++i)
        {
            pointCoord(0) = lowValues(0) + i * dSize;
            pointCoord(1) = lowValues(1) + j * dSize;
            double fValue = fInterpol(pointCoord);
            shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
            double interpValue = interpolator->apply(hierarValues);
            double error = fabs(fValue - interpValue);
            errMax = max(errMax, error);
        }
}


BOOST_AUTO_TEST_CASE(testSparseAdaptive2DNoBound)
{
    {
        int level = 4;
        int degree = 1 ;
        double precision = 1e-5;
        testSparseNoBoundAdaptive2D(level, degree, precision);
    }
    {
        int level = 3;
        int degree = 2 ;
        double precision = 1e-5;
        testSparseNoBoundAdaptive2D(level, degree, precision);
    }
    {
        int level = 3;
        int degree = 3 ;
        double precision = 1e-5;
        testSparseNoBoundAdaptive2D(level, degree, precision);
    }
}


// to test generic precision
void testSparseBoundAdaptive5D(int p_level, int p_degree, double p_precision)
{
    ArrayXd  lowValues(5);
    lowValues(0) = -1;
    lowValues(1) = 0;
    lowValues(2) = 0;
    lowValues(3) = 0;
    lowValues(4) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(5, 2.);
    ArrayXd  weight =   ArrayXd::Constant(5, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return p_x(0) * p_x(0) + p_x(0) * p_x(0) * exp(p_x(2)) * exp(p_x(3)) * exp(p_x(4));
        else
            return - p_x(0) * exp(p_x(1)) * exp(p_x(2)) * exp(p_x(3)) * exp(p_x(4)) ;
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();
    function< double(const vector< double> &)> fErrorLevels = functionMaxLevel();
    //  adaptation
    sparseGrid.refine(p_precision, fInterpol, fErrorOneLevel, fErrorLevels, valuesFunction, hierarValues) ;

    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(5);
    int bptDim = 7;
    double dSize = sizeDomain(0) / bptDim;
    for (int m = 0; m <= bptDim; ++m)
        for (int l = 0; l <= bptDim; ++l)
            for (int k = 0; k <= bptDim; ++k)
                for (int j = 0; j <= bptDim; ++j)
                    for (int i = 0; i <= bptDim; ++i)
                    {
                        pointCoord(0) = lowValues(0) + i * dSize;
                        pointCoord(1) = lowValues(1) + j * dSize;
                        pointCoord(2) = lowValues(2) + k * dSize;
                        pointCoord(3) = lowValues(3) + l * dSize;
                        pointCoord(4) = lowValues(4) + m * dSize;

                        double fValue = fInterpol(pointCoord);
                        shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
                        double interpValue = interpolator->apply(hierarValues);
                        double error = fabs(fValue - interpValue);
                        errMax = max(errMax, error);
                    }
    BOOST_CHECK(errMax < p_precision);
}

BOOST_AUTO_TEST_CASE(testSparseAdaptive5DBound)
{
    int level = 6;
    int degree = 1 ;
    double precision = 1e-2;
    testSparseBoundAdaptive5D(level, degree, precision);
}


// to test generic precision
void testSparseBoundAdaptive4D(int p_level, int p_degree, double p_precisionRefine, double p_precisionCoarsen1, double p_precisionCoarsen2)
{
    ArrayXd  lowValues(4);
    lowValues(0) = -1;
    lowValues(1) = 0;
    lowValues(2) = 0;
    lowValues(3) = 0;
    ArrayXd  sizeDomain = ArrayXd::Constant(4, 2.);
    ArrayXd  weight =   ArrayXd::Constant(4, 1.);

    // function value to interpol
    auto f([](const ArrayXd & p_x)
    {
        if (p_x(0) > 0.)
            return p_x(0) * p_x(0) + p_x(0) * p_x(0) * exp(p_x(2)) * exp(p_x(3));
        else
            return - p_x(0) * exp(p_x(1)) * exp(p_x(2)) * exp(p_x(3)) ;
    });
    function<double(const ArrayXd &p_x)> fInterpol(cref(f));

    // sparse grid generation
    SparseSpaceGridBound sparseGrid(lowValues, sizeDomain, p_level, weight, p_degree);

    // first hierarchize initial level
    shared_ptr<GridIterator > iterGrid = sparseGrid.getGridIterator();

    // nodal values
    ArrayXd valuesFunction(sparseGrid.getNbPoints());
    while (iterGrid->isValid())
    {
        ArrayXd pointCoord = iterGrid->getCoordinate();
        valuesFunction(iterGrid->getCount()) = fInterpol(pointCoord);
        iterGrid->next();
    }
    // Hieriarchize
    ArrayXd hierarValues = valuesFunction;
    sparseGrid.toHierarchize(hierarValues);

    // function to calculate errors
    function< double(const SparseSet::const_iterator &,  const ArrayXd &)> fErrorOneLevel = functionMaxLevel();
    function< double(const vector< double> &)> fErrorLevels = functionMaxLevel();

    // coarsen

    // coarsen the grid
    sparseGrid.coarsen(p_precisionCoarsen1,  fErrorOneLevel, valuesFunction, hierarValues) ;
    //  refinement
    sparseGrid.refine(p_precisionRefine, fInterpol, fErrorOneLevel, fErrorLevels, valuesFunction, hierarValues) ;
    // coarsen
    sparseGrid.coarsen(p_precisionCoarsen2,  fErrorOneLevel, valuesFunction, hierarValues) ;

    // calculate max error on regular grid
    double errMax = 0;;
    ArrayXd pointCoord(4);
    int bptDim = 10;
    double dSize = sizeDomain(0) / bptDim;
    for (int l = 0; l <= bptDim; ++l)
        for (int k = 0; k <= bptDim; ++k)
            for (int j = 0; j <= bptDim; ++j)
                for (int i = 0; i <= bptDim; ++i)
                {
                    pointCoord(0) = lowValues(0) + i * dSize;
                    pointCoord(1) = lowValues(1) + j * dSize;
                    pointCoord(2) = lowValues(2) + k * dSize;
                    pointCoord(3) = lowValues(3) + l * dSize;

                    double fValue = fInterpol(pointCoord);
                    shared_ptr<Interpolator > interpolator = sparseGrid.createInterpolator(pointCoord);
                    double interpValue = interpolator->apply(hierarValues);
                    double error = fabs(fValue - interpValue);
                    errMax = max(errMax, error);
                }
    BOOST_CHECK(errMax < p_precisionCoarsen2 * 10);
}

BOOST_AUTO_TEST_CASE(testSparseAdaptive4DBound)
{
    int level = 5;
    int degree = 2 ;
    double precisionRefine = 1e-2;
    double precisionCoarsen1 = 1e-1;
    double precisionCoarsen2 = 1e-2;
    testSparseBoundAdaptive4D(level, degree, precisionRefine, precisionCoarsen1, precisionCoarsen2);
}
