// Copyright (C) 2016 Fime
// All Rights Reserved
// This code is published under the GNU Lesser General Public License (GNU LGPL)
#define BOOST_TEST_DYN_LINK
#define _USE_MATH_DEFINES
#include <math.h>
#include <functional>
#include <memory>
#include <boost/test/unit_test.hpp>
#include <boost/mpi.hpp>
#include <Eigen/Dense>
#include "StOpt/core/grids/OneDimRegularSpaceGrid.h"
#include "StOpt/core/grids/OneDimSpaceGrid.h"
#include "StOpt/core/grids/OneDimData.h"
#include "StOpt/core/grids/RegularSpaceGridGeners.h"
#include "StOpt/regression/LocalLinearRegression.h"
#include "StOpt/regression/LocalLinearRegressionGeners.h"
#include "test/c++/tools/simulators/MeanRevertingSimulator.h"
#include "test/c++/tools/dp/DynamicProgrammingByRegressionDist.h"
#include "test/c++/tools/dp/SimulateRegressionDist.h"
#include "test/c++/tools/dp/SimulateRegressionControlDist.h"
#include "test/c++/tools/dp/OptimizeGasStorageSwitchingCost.h"

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

/// 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 = 0.5;

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



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

BOOST_AUTO_TEST_CASE(testSwitchingVaryingRegimeStorageDist)
{
    // storage
    /////////
    double maxLevelStorage  = 360000;
    double injectionRateStorage = 60000;
    double withdrawalRateStorage = 45000;
    double injectionCostStorage = 0.35;
    double withdrawalCostStorage = 0.35;
    double switchingCostStorage =  4. ;

    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);
    // regime values allowed
    ArrayXd tvalues(6);
    tvalues << 0., 1e-3, 1. / 4 + 1e-3, 1. / 2 + 1e-3, 3. / 4. + 1e-3, 1. ;
    shared_ptr<OneDimSpaceGrid> timeRegimes = make_shared<OneDimSpaceGrid>(tvalues);
    shared_ptr<vector< int  > > regValues = make_shared<vector< int > >(6);
    (*regValues)[0] = 3;
    (*regValues)[1] = 3;
    (*regValues)[2] = 1;
    (*regValues)[3] = 3 ;
    (*regValues)[4] = 2;
    (*regValues)[5] = 3.;
    shared_ptr<OneDimData<OneDimSpaceGrid, int > > regime = make_shared<OneDimData<OneDimSpaceGrid, int > >(timeRegimes, regValues);
    // 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;
    // grid
    //////
    int nGrid = 40;
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd step = ArrayXd::Constant(1, maxLevelStorage / nGrid);
    ArrayXi nbStep = ArrayXi::Constant(1, nGrid);
    shared_ptr<RegularSpaceGrid> grid = make_shared<RegularSpaceGrid>(lowValues, step, nbStep);

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

    // initial values
    ArrayXd initialStock = ArrayXd::Constant(1, maxLevelStorage);
    int initialRegime = 0; //  here do nothing (no injection, no withdrawal)

    boost::mpi::communicator world;
    // Optimize
    ///////////
    string fileToDump = "CondExpGas" + to_string(world.size());
    bool bOneFile = true;
    // link the simulations to the optimizer
    storage->setSimulator(backSimulator);
    double valueOptimDist =  DynamicProgrammingByRegressionDist(grid, storage, regressor, vFunction, initialStock, initialRegime, fileToDump, bOneFile, world);

    world.barrier();
    // a forward simulator
    ///////////////////////
    int nbsimulSim = 40000;
    bForward = true;
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator(new	  MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > (futureGrid, sigma, mr, r, maturity, nstep, nbsimulSim, bForward));
    // link the simulations to the optimizer
    storage->setSimulator(forSimulator);
    double valSimuDist = SimulateRegressionDist(grid, storage, vFunction, initialStock, initialRegime, fileToDump, bOneFile, world) ;

    if (world.rank() == 0)
    {
        cout << " valSimuDist " << valSimuDist << " valOP " << valueOptimDist << endl ;
        BOOST_CHECK_CLOSE(valueOptimDist, valSimuDist, accuracyClose);
    }

    // a forward simulator
    ///////////////////////
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator2(new	  MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > (futureGrid, sigma, mr,  r, maturity, nstep, nbsimulSim, bForward));
    // link the simulations to the optimizer
    storage->setSimulator(forSimulator2);
    double valSimuDist2 = SimulateRegressionControlDist(grid, storage, vFunction, initialStock, initialRegime, fileToDump, bOneFile, world) ;

    if (world.rank() == 0)
    {
        cout << " valSimuDist2 " << valSimuDist2 << " valOP " << valueOptimDist << endl ;
        BOOST_CHECK_CLOSE(valueOptimDist, valSimuDist2, accuracyClose);
    }
}

BOOST_AUTO_TEST_CASE(testSwitchingStorageDist)
{
    // storage
    /////////
    double maxLevelStorage  = 360000;
    double injectionRateStorage = 60000;
    double withdrawalRateStorage = 45000;
    double injectionCostStorage = 0.35;
    double withdrawalCostStorage = 0.35;
    double switchingCostStorage =  4. ;

    double maturity = 1.;
    size_t nstep = 10;
    // define  a time grid
    shared_ptr<OneDimRegularSpaceGrid> timeGrid(new OneDimRegularSpaceGrid(0., maturity / nstep, nstep));
    // future values
    shared_ptr<vector< double > > futValues(new 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(new 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;
    // grid
    //////
    int nGrid = 40;
    ArrayXd lowValues = ArrayXd::Constant(1, 0.);
    ArrayXd step = ArrayXd::Constant(1, maxLevelStorage / nGrid);
    ArrayXi nbStep = ArrayXi::Constant(1, nGrid);
    shared_ptr<RegularSpaceGrid> grid = make_shared<RegularSpaceGrid>(lowValues, step, nbStep);

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

    // initial values
    ArrayXd initialStock = ArrayXd::Constant(1, maxLevelStorage);
    int initialRegime = 0; //  here do nothing (no injection, no withdrawal)

    boost::mpi::communicator world;
    // Optimize
    ///////////
    string fileToDump = "CondExpGas" + to_string(world.size());
    bool bOneFile = true;
    // link the simulations to the optimizer
    storage->setSimulator(backSimulator);
    double valueOptimDist =  DynamicProgrammingByRegressionDist(grid, storage, regressor, vFunction, initialStock, initialRegime, fileToDump, bOneFile, world);

    world.barrier();
    // a forward simulator
    ///////////////////////
    int nbsimulSim = 40000;
    bForward = true;
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator(new	  MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > (futureGrid, sigma, mr, r, maturity, nstep, nbsimulSim, bForward));
    // link the simulations to the optimizer
    storage->setSimulator(forSimulator);
    double valSimuDist = SimulateRegressionDist(grid, storage, vFunction, initialStock, initialRegime, fileToDump, bOneFile, world) ;

    if (world.rank() == 0)
    {
        cout << " valSimuDist " << valSimuDist << " valOP " << valueOptimDist << endl ;
        BOOST_CHECK_CLOSE(valueOptimDist, valSimuDist, accuracyClose);
    }
    // a forward simulator
    ///////////////////////
    shared_ptr< MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > > forSimulator2(new	  MeanRevertingSimulator< OneDimData<OneDimRegularSpaceGrid, double> > (futureGrid, sigma, mr, r, maturity, nstep, nbsimulSim, bForward));
    // link the simulations to the optimizer
    storage->setSimulator(forSimulator2);
    double valSimuDist2 = SimulateRegressionControlDist(grid, storage, vFunction, initialStock, initialRegime, fileToDump, bOneFile, world) ;

    if (world.rank() == 0)
    {
        cout << " valSimuDist2 " << valSimuDist2 << " valOP " << valueOptimDist << endl ;
        BOOST_CHECK_CLOSE(valueOptimDist, valSimuDist2, accuracyClose);
    }

}

// (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);
}
