// Copyright (C) 2016 EDF
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#ifndef USE_MPI
#define BOOST_TEST_MODULE testGasStorage
#endif
#define BOOST_TEST_DYN_LINK
#ifdef USE_MPI
#include <boost/mpi.hpp>
#endif
#define _USE_MATH_DEFINES
#include <math.h>
#include <memory>
#include <functional>
#include <boost/test/unit_test.hpp>
#include <boost/timer/timer.hpp>
#include <Eigen/Dense>
#include "StOpt/core/grids/OneDimRegularSpaceGrid.h"
#include "StOpt/core/grids/OneDimData.h"
#include "StOpt/regression/LocalLinearRegression.h"
#include "StOpt/core/grids/RegularSpaceGridGeners.h"
#include "StOpt/core/grids/RegularLegendreGridGeners.h"
#include "StOpt/regression/LocalLinearRegressionGeners.h"
#include "test/c++/tools/simulators/MeanRevertingSimulator.h"
#include "test/c++/tools/dp/DynamicProgrammingByRegression.h"
#include "test/c++/tools/dp/DynamicProgrammingByRegressionSparse.h"
#include "test/c++/tools/dp/SimulateRegression.h"
#include "test/c++/tools/dp/SimulateRegressionControl.h"
#include "test/c++/tools/dp/OptimizeGasStorage.h"

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


#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
{
string normalize_test_case_name(const_string name)
{
    return (name[0] == '&' ? string(name.begin() + 1, name.size() - 1) : string(name.begin(), name.size()));
}
}
}
}

double accuracyClose =  1.5;

class ZeroFunction
{
public:
    ZeroFunction() {}
    double operator()(const int &, const ArrayXd &, const ArrayXd &) const
    {
        return 0. ;
    }
};


BOOST_AUTO_TEST_CASE(testOrnsteinUhlenbeckSimulator)
{
    size_t nstep = 100;
    // define  a time grid
    shared_ptr<OneDimRegularSpaceGrid> timeGrid = make_shared<OneDimRegularSpaceGrid>(0., 1. / nstep, nstep);
    // future values
    shared_ptr<vector< double > > futValues = make_shared<vector<double> >(nstep + 1);
    // periodicity factor
    int iPeriod = 52;
    for (size_t i = 0; i < nstep + 1; ++i)
        (*futValues)[i] = 2. + sin((M_PI * i * iPeriod) / nstep);
    // define the future curve
    shared_ptr<OneDimData<OneDimRegularSpaceGrid, double> > futureGrid = make_shared<OneDimData< OneDimRegularSpaceGrid, double> >(timeGrid, futValues);
    // two dimensional factors
    int nDim = 2;
    VectorXd sigma = VectorXd::Constant(nDim, 0.2);
    VectorXd mr = VectorXd::Constant(nDim, 0.1);
    // maturity
    double T = 1.;
    // number of simulations
    size_t nbsimul = 1e4;
    // a forward simulator
    bool bForward = true;
    double r = 0.05 ; // interest rate
    MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> >  forwardSimulator(futureGrid, sigma, mr, r, T, nstep, nbsimul, bForward);
    int nnstep = 10;
    for (size_t istep = 0; istep < nstep / nnstep; ++istep)
    {
        // one step forward
        for (int j = 0; j < nnstep; ++j)
            forwardSimulator.stepForward();
        BOOST_CHECK_CLOSE(forwardSimulator.getAssetValues().mean(), (*futValues)[(istep + 1)*nnstep], accuracyClose);
    }
    // backward simulator
    bForward = false;
    MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> >  backwardSimulator(futureGrid, sigma, mr, r, T, nstep, nbsimul, bForward);
    for (size_t istep = 0; istep < nstep / nnstep; ++istep)
    {
        // one step backward
        for (int j = 0; j < nnstep; ++j)
            backwardSimulator.stepBackward();
        BOOST_CHECK_CLOSE(backwardSimulator.getAssetValues().mean(), (*futValues)[ nstep - (istep + 1)*nnstep], accuracyClose);
    }
}


/// \brief valorization of a given gas storage on a  grid
/// \param p_grid             the grid
/// \param p_maxLevelStorage  maximum level
void testGasStorageFullGrid(shared_ptr< FullGrid > &p_grid, const double &p_maxLevelStorage)
{
#ifdef USE_MPI
    boost::mpi::communicator world;
#endif

    // storage
    /////////
    double injectionRateStorage = 60000;
    double withdrawalRateStorage = 45000;
    double injectionCostStorage = 0.35;
    double withdrawalCostStorage = 0.35;

    double maturity = 1.;
    size_t nstep = 100;
    // define  a time grid
    shared_ptr<OneDimRegularSpaceGrid> timeGrid = make_shared<OneDimRegularSpaceGrid>(0., maturity / nstep, nstep);
    // future values
    shared_ptr<vector< double > > futValues = make_shared<vector<double> >(nstep + 1);
    // periodicity factor
    int iPeriod = 52;
    for (size_t i = 0; i < nstep + 1; ++i)
        (*futValues)[i] = 50. + 20 * sin((M_PI * i * iPeriod) / nstep);
    // define the future curve
    shared_ptr<OneDimData<OneDimRegularSpaceGrid, double> > futureGrid = make_shared<OneDimData< OneDimRegularSpaceGrid, double> >(timeGrid, futValues);
    // one dimensional factors
    int nDim = 1;
    VectorXd sigma = VectorXd::Constant(nDim, 0.94);
    VectorXd mr = VectorXd::Constant(nDim, 0.29);
    // number of simulations
    size_t nbsimulOpt = 20000;

    // a backward simulator
    ///////////////////////
    bool bForward = false;
    double r = 0.; // interest rate
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > backSimulator = make_shared<MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > >(futureGrid, sigma, mr, r, maturity, nstep, nbsimulOpt, bForward);
    // optimizer
    ///////////
    shared_ptr< OptimizeGasStorage< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > > storage = make_shared< OptimizeGasStorage< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > >(injectionRateStorage, withdrawalRateStorage, injectionCostStorage, withdrawalCostStorage);
    // regressor
    ///////////
    int nMesh = 6;
    ArrayXi nbMesh = ArrayXi::Constant(1, nMesh);
    shared_ptr< LocalLinearRegression > regressor = make_shared<LocalLinearRegression>(nbMesh);
    // final value
    function<double(const int &, const ArrayXd &, const ArrayXd &)>  vFunction = ZeroFunction();

    // initial values
    ArrayXd initialStock = ArrayXd::Constant(1, p_maxLevelStorage);
    int initialRegime = 0; // only one regime

    // Optimize
    ///////////
    string fileToDump = "CondExpGasStorage";
    double valueOptim ;
    {
        // link the simulations to the optimizer
        storage->setSimulator(backSimulator);
        boost::timer::auto_cpu_timer t;

        valueOptim =  DynamicProgrammingByRegression(p_grid, storage, regressor, vFunction, initialStock, initialRegime, fileToDump
#ifdef USE_MPI
                      , world
#endif
                                                    );
    }
    cout << "valueOptim " << valueOptim << endl ;
    // a forward simulator
    ///////////////////////
    int nbsimulSim = 40000;
    bForward = true;
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator = make_shared<MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > (futureGrid, sigma, mr, r, maturity, nstep, nbsimulSim, bForward);
    double valSimu ;
    {
        // link the simulations to the optimizer
        storage->setSimulator(forSimulator);
        boost::timer::auto_cpu_timer t;
        valSimu = SimulateRegression(p_grid, storage, vFunction, initialStock, initialRegime, fileToDump
#ifdef USE_MPI
                                     , world
#endif
                                    ) ;

    }
    cout << " valSimu  " << valSimu << " valueOptim " << valueOptim << endl ;
    BOOST_CHECK_CLOSE(valueOptim, valSimu, accuracyClose);

    /// a second forward simulator
    /////////////////////////////
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator2 = make_shared<MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > (futureGrid, sigma, mr, r, maturity, nstep, nbsimulSim, bForward);

    double valSimu2 ;
    {
        // link the simulations to the optimizer
        storage->setSimulator(forSimulator2);
        boost::timer::auto_cpu_timer t;
        valSimu2 = SimulateRegressionControl(p_grid, storage, vFunction, initialStock, initialRegime, fileToDump
#ifdef USE_MPI
                                             , world
#endif
                                            ) ;

    }
    cout << " valSimu2  " << valSimu2 << " valueOptim " << valueOptim << endl ;
    BOOST_CHECK_CLOSE(valueOptim, valSimu2, accuracyClose);
}

BOOST_AUTO_TEST_CASE(testSimpleStorage)
{
    // storage
    /////////
    double maxLevelStorage  = 90000;
    // grid
    //////
    int nGrid = 10;
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd step = ArrayXd::Constant(1, maxLevelStorage / nGrid);
    ArrayXi nbStep = ArrayXi::Constant(1, nGrid);
    shared_ptr<FullGrid> grid = make_shared<RegularSpaceGrid>(lowValues, step, nbStep);

    testGasStorageFullGrid(grid, maxLevelStorage);
}

BOOST_AUTO_TEST_CASE(testSimpleStorageLegendreLinear)
{
    // storage
    /////////
    double maxLevelStorage  = 90000;
    // grid
    //////
    int nGrid = 10;
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd step = ArrayXd::Constant(1, maxLevelStorage / nGrid);
    ArrayXi nbStep = ArrayXi::Constant(1, nGrid);
    ArrayXi poly = ArrayXi::Constant(1, 1);
    shared_ptr<FullGrid> grid = make_shared<RegularLegendreGrid>(lowValues, step, nbStep, poly);

    testGasStorageFullGrid(grid, maxLevelStorage);
}
BOOST_AUTO_TEST_CASE(testSimpleStorageLegendreQuadratic)
{
    // storage
    /////////
    double maxLevelStorage  = 90000;
    // grid
    //////
    int nGrid = 5;
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd step = ArrayXd::Constant(1, maxLevelStorage / nGrid);
    ArrayXi nbStep = ArrayXi::Constant(1, nGrid);
    ArrayXi poly = ArrayXi::Constant(1, 2);
    shared_ptr<FullGrid> grid = make_shared<RegularLegendreGrid>(lowValues, step, nbStep, poly);

    testGasStorageFullGrid(grid, maxLevelStorage);
}


BOOST_AUTO_TEST_CASE(testSimpleStorageLegendreCubic)
{
    // storage
    /////////
    double maxLevelStorage  = 90000;
    // grid
    //////
    int nGrid = 5;
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd step = ArrayXd::Constant(1, maxLevelStorage / nGrid);
    ArrayXi nbStep = ArrayXi::Constant(1, nGrid);
    ArrayXi poly = ArrayXi::Constant(1, 3);
    shared_ptr<FullGrid> grid = make_shared<RegularLegendreGrid>(lowValues, step, nbStep, poly);

    testGasStorageFullGrid(grid, maxLevelStorage);
}



/// \brief valorization of a given gas storage on a  grid
/// \param p_grid             the grid
/// \param p_maxLevelStorage  maximum level
void testGasStorageSparseGrid(shared_ptr< SparseSpaceGrid > &p_grid, const double &p_maxLevelStorage)
{
#ifdef USE_MPI
    boost::mpi::communicator world;
#endif

    // storage
    /////////
    double injectionRateStorage = 60000;
    double withdrawalRateStorage = 45000;
    double injectionCostStorage = 0.35;
    double withdrawalCostStorage = 0.35;

    double maturity = 1.;
    size_t nstep = 10;
    // define  a time grid
    shared_ptr<OneDimRegularSpaceGrid> timeGrid = make_shared<OneDimRegularSpaceGrid>(0., maturity / nstep, nstep);
    // future values
    shared_ptr<vector< double > > futValues = make_shared< vector<double> >(nstep + 1);
    // periodicity factor
    int iPeriod = 52;
    for (size_t i = 0; i < nstep + 1; ++i)
        (*futValues)[i] = 50. + 20 * sin((M_PI * i * iPeriod) / nstep);
    // define the future curve
    shared_ptr<OneDimData<OneDimRegularSpaceGrid, double> > futureGrid =  make_shared<OneDimData< OneDimRegularSpaceGrid, double> > (timeGrid, futValues);
    // one dimensional factors
    int nDim = 1;
    VectorXd sigma = VectorXd::Constant(nDim, 0.94);
    VectorXd mr = VectorXd::Constant(nDim, 0.29);
    // number of simulations
    size_t nbsimulOpt = 20000;
    // interest rate
    double r = 0. ;
    // a backward simulator
    ///////////////////////
    bool bForward = false;
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > backSimulator = make_shared<MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> >  >(futureGrid, sigma, mr, r, maturity, nstep, nbsimulOpt, bForward);
    // optimizer
    ///////////
    shared_ptr< OptimizeGasStorage< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > > storage = make_shared< OptimizeGasStorage< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > >(injectionRateStorage, withdrawalRateStorage, injectionCostStorage, withdrawalCostStorage);
    // regressor
    ///////////
    int nMesh = 6;
    ArrayXi nbMesh = ArrayXi::Constant(1, nMesh);
    shared_ptr< LocalLinearRegression > regressor = make_shared<LocalLinearRegression>(nbMesh);
    // final value
    function<double(const int &, const ArrayXd &, const ArrayXd &)>  vFunction = ZeroFunction();

    // initial values
    ArrayXd initialStock = ArrayXd::Constant(1, p_maxLevelStorage);
    int initialRegime = 0; // only one regime

    // Optimize
    ///////////
    string fileToDump = "CondExpGasStorage";
    double valueOptimSparse ;
    {
        // link the simulations to the optimizer
        storage->setSimulator(backSimulator);
        boost::timer::auto_cpu_timer t;

        valueOptimSparse =  DynamicProgrammingByRegressionSparse(p_grid, storage, regressor, vFunction, initialStock, initialRegime, fileToDump
#ifdef USE_MPI
                            , world
#endif

                                                                );

        cout << " valueOptimSparse " << valueOptimSparse << endl ;
    }
    // a forward simulator
    ///////////////////////
    int nbsimulSim = 40000;
    bForward = true;
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator = make_shared<MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > (futureGrid, sigma, mr, r, maturity, nstep, nbsimulSim, bForward);
    double valSimuSparse ;
    {
        // link the simulations to the optimizer
        storage->setSimulator(forSimulator);
        boost::timer::auto_cpu_timer t;
        valSimuSparse = SimulateRegression(p_grid, storage, vFunction, initialStock, initialRegime, fileToDump
#ifdef USE_MPI
                                           , world
#endif

                                          ) ;

    }
    cout << " valSimuSparse  " << valSimuSparse << " valueOptimSparse " << valueOptimSparse << endl ;
    BOOST_CHECK_CLOSE(valueOptimSparse, valSimuSparse, accuracyClose);

    /// a second forward simulator
    /////////////////////////////
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator2 = make_shared<MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > (futureGrid, sigma, mr, r,  maturity, nstep, nbsimulSim, bForward);

    double valSimuSparse2 ;
    {
        // link the simulations to the optimizer
        storage->setSimulator(forSimulator2);
        boost::timer::auto_cpu_timer t;
        valSimuSparse2 = SimulateRegressionControl(p_grid, storage, vFunction, initialStock, initialRegime, fileToDump
#ifdef USE_MPI
                         , world
#endif
                                                  ) ;

    }
    cout << " valSimuSparse2  " << valSimuSparse2 << " valueOptimSparse " << valueOptimSparse << endl ;
    BOOST_CHECK_CLOSE(valueOptimSparse, valSimuSparse2, accuracyClose);
}
BOOST_AUTO_TEST_CASE(testSimpleStorageSparse)
{
    // storage
    /////////
    double maxLevelStorage  = 90000;
    // grid
    //////
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd sizeDomain = ArrayXd::Constant(1, maxLevelStorage);
    ArrayXd  weight = ArrayXd::Constant(1, 1.);
    int level = 4;
    size_t degree = 1;
    shared_ptr<SparseSpaceGrid> grid = make_shared<SparseSpaceGridBound>(lowValues, sizeDomain, level, weight, degree);

    testGasStorageSparseGrid(grid, maxLevelStorage);
}

#ifdef USE_MPI
// (empty) Initialization function. Can't use testing tools here.
bool init_function()
{
    return true;
}

int main(int argc, char *argv[])
{
#if defined   __linux
    enable_abort_on_floating_point_exception();
#endif
    boost::mpi::environment env(argc, argv);
    return ::boost::unit_test::unit_test_main(&init_function, argc, argv);
}

#endif
