/**
 * Copyright (c) 2011, 2012
 * Claudio Kopper <claudio.kopper@icecube.wisc.edu>
 * and the IceCube Collaboration <http://www.icecube.wisc.edu>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 *
 * $Id: I3CLSimFunctionFromTable.cxx 179360 2020-03-10 16:07:35Z eganster $
 *
 * @file I3CLSimFunctionFromTable.cxx
 * @version $Revision: 179360 $
 * @date $Date: 2020-03-10 10:07:35 -0600 (Tue, 10 Mar 2020) $
 * @author Claudio Kopper
 */

#define __CL_ENABLE_EXCEPTIONS
#include "clsim/cl.hpp"

#include <icetray/serialization.h>
#include <clsim/function/I3CLSimFunctionFromTable.h>

#include <typeinfo>
#include <cmath>
#include <math.h>
#include <stdexcept>
#include <algorithm>
#include <functional>

#include <boost/lexical_cast.hpp>
#include <boost/foreach.hpp>

#include "clsim/I3CLSimHelperToFloatString.h"
using namespace I3CLSimHelper;

// when storing data in 16bits, this class can optionally
// use IEEE half precision. This is not used by default,
// but can be activated using this ifdef:
 //#define USE_OPENCL_HALF_PRECISION

#ifdef USE_OPENCL_HALF_PRECISION
#include "opencl/ieeehalfprecision.h"
#endif

const bool I3CLSimFunctionFromTable::default_storeDataAsHalfPrecision=false;

I3CLSimFunctionFromTable::
I3CLSimFunctionFromTable(const std::vector<double> &wlens,
                         const std::vector<double> &values,
                         bool storeDataAsHalfPrecision)
:
wlenStep_(NAN),
wlens_(wlens),
values_(values),
equalSpacingMode_(false),
storeDataAsHalfPrecision_(storeDataAsHalfPrecision)
{
    if (wlens_.size() < 2) throw std::range_error("wlens must contain at least 2 elements!");
    if (wlens_.size() != values_.size()) throw std::range_error("wlens and values must have the same size!");

    startWlen_ = wlens_[0];
}

I3CLSimFunctionFromTable::
I3CLSimFunctionFromTable(double startWlen,
                         double wlenStep,
                         const std::vector<double> &values,
                         bool storeDataAsHalfPrecision)
:
startWlen_(startWlen),
wlenStep_(wlenStep),
values_(values),
equalSpacingMode_(true),
storeDataAsHalfPrecision_(storeDataAsHalfPrecision)
{
    if (wlenStep_ <= 0.) throw std::range_error("wlenStep must not be <= 0!");
    if (values_.size() < 2) throw std::length_error("values must contain at least 2 elements!");

    // fill the wavelength array, even though we don't really need it in this mode
    wlens_.clear();
    for (std::size_t i=0;i<values_.size();++i) {
        wlens_.push_back(startWlen_ + static_cast<double>(i)*wlenStep_);
    }
}

I3CLSimFunctionFromTable::I3CLSimFunctionFromTable() {;}

I3CLSimFunctionFromTable::~I3CLSimFunctionFromTable()
{;}

namespace {
    inline double mix(double min, double max, double t)
    {
        return min+(max-min)*t;
    }
}

double I3CLSimFunctionFromTable::GetValue(double wlen) const
{
    if (equalSpacingMode_) {
        double fbin;
        double fraction = modf((wlen-startWlen_)/wlenStep_, &fbin);
        int ibin=static_cast<int>(fbin);

        if ((ibin<0) || ((ibin==0) && (fraction<0)))  {
            ibin=0;
            fraction=0.;
        } else if (static_cast<std::size_t>(ibin)>=values_.size()-1) {
            ibin=values_.size()-2;
            fraction=1.;
        }
        const unsigned int bin = ibin;

        return mix(values_[bin],
                   values_[bin+1],
                   fraction);
    } else {
        if (wlen <= wlens_[0]) {
            return values_[0];
        }

        std::size_t bin;
        double fraction;
        for (std::size_t i=1;i<wlens_.size();++i) {
            if (wlen <= wlens_[i]) {
                // got it!
                bin=i-1;
                fraction = (wlen-wlens_[i-1])/(wlens_[i]-wlens_[i-1]);

                return mix(values_[bin],
                           values_[bin+1],
                           fraction);
            }
        }

        // nothing in range
        return values_[wlens_.size()-1];
    }
}

double I3CLSimFunctionFromTable::GetMinWlen() const
{
    if (equalSpacingMode_) {
        return startWlen_;
    } else {
        return wlens_[0];
    }
}

double I3CLSimFunctionFromTable::GetMaxWlen() const
{
    if (equalSpacingMode_) {
        return startWlen_ + wlenStep_ * static_cast<double>(values_.size()-1);
    } else {
        return wlens_[wlens_.size()-1];
    }
}

std::string I3CLSimFunctionFromTable::GetOpenCLFunction(const std::string &functionName) const
{
    if (!equalSpacingMode_)
        log_fatal("GetOpenCLFunction() has not yet been implemented for non-equal bin spacings.");

    // some names
    const std::string dataName = functionName + "_data";
    const std::string interpHelperName = functionName + "_getInterpolationBinAndFraction";

#ifndef USE_OPENCL_HALF_PRECISION
    double smallestEntry=0., largestEntry=0.;
#endif

    std::string dataDef;
    if (storeDataAsHalfPrecision_)
    {
#ifdef USE_OPENCL_HALF_PRECISION
        // define the actual data
        dataDef = dataDef +
        "// this is IEEE half precision values stored as 16bit integers:\n" +
        "__constant unsigned short " + dataName + "[" + boost::lexical_cast<std::string>(values_.size()) + "] = {\n";
        BOOST_FOREACH(const double &val, values_)
        {
            // convert the double value to IEEE half precision and store it as an unsigned integer (16bit)
            uint16_t dummy=0;
            doubles2halfp(&dummy, &val, 1);
            dataDef += boost::lexical_cast<std::string>(dummy) + ", ";
        }
        dataDef += "\n";
        dataDef += "};\n\n";
#else
        // find the smallest and largest entries.
        bool firstIteration=true;
        BOOST_FOREACH(const double &val, values_)
        {
            if (firstIteration) {
                firstIteration=false;
                smallestEntry=val;
                largestEntry=val;
                continue;
            }

            if (val < smallestEntry) smallestEntry=val;
            if (val > largestEntry) largestEntry=val;
        }

        // define the actual data
        dataDef = dataDef +
        "#define " + dataName + "_SMALLEST_ENTRY " + ToFloatString(smallestEntry) + " \n" +
        "#define " + dataName + "_LARGEST_ENTRY " + ToFloatString(largestEntry) + " \n" +
        "__constant unsigned short " + dataName + "[" + boost::lexical_cast<std::string>(values_.size()) + "] = {\n";
        BOOST_FOREACH(const double &val, values_)
        {
            uint16_t dummy = static_cast<uint16_t>(65535.*(val-smallestEntry)/(largestEntry-smallestEntry));
            dataDef += boost::lexical_cast<std::string>(dummy) + ", ";
        }
        dataDef += "\n";
        dataDef += "};\n\n";
#endif
    }
    else
    {
        // define the actual data
        dataDef = dataDef +
        "__constant float " + dataName + "[" + boost::lexical_cast<std::string>(values_.size()) + "] = {\n";
        BOOST_FOREACH(const double &val, values_)
        {
            dataDef += ToFloatString(val) + ", ";
        }
        dataDef += "\n";
        dataDef += "};\n\n";
    }

    std::string interpHelperDef =
    std::string("inline void ") + interpHelperName + "(float wavelength, int *bin, float *fraction)";

    std::string interpHelperBody =
    "{\n"
    "    float fbin;\n"
    "    *fraction = modf((wavelength - " + ToFloatString(startWlen_) + ")/" + ToFloatString(wlenStep_) + ", &fbin);\n"
    "    \n"
    "    int ibin=(int)fbin;\n"
    "    \n"
    "    if ((ibin<0) || ((ibin==0) && (*fraction<0))) {\n"
    "        ibin=0;\n"
    "        *fraction=0.f;\n"
    "    } else if (ibin>=" + boost::lexical_cast<std::string>(values_.size()) + "-1) {\n"
    "        ibin=" + boost::lexical_cast<std::string>(values_.size()) + "-2;\n"
    "        *fraction=1.f;\n"
    "    }\n"
    "    \n"
    "    *bin = ibin;\n"
    "}\n\n"
    ;

    std::string funcDef =
    std::string("inline float ") + functionName + std::string("(float wavelength)\n");

    std::string funcBody;
    if (storeDataAsHalfPrecision_)
    {
#ifdef USE_OPENCL_HALF_PRECISION
        funcBody = funcBody +
        "{\n"
        "    int bin; float fraction;\n"
        "    " + interpHelperName + "(wavelength, &bin, &fraction);\n"
        "    \n"
        "    return mix(vload_half(bin,   (const __constant half *)" + dataName + "),\n"
        "               vload_half(bin+1, (const __constant half *)" + dataName + "),\n"
        "               fraction);\n"
        "}\n"
        ;
#else
        funcBody = funcBody +
        "{\n"
        "    int bin; float fraction;\n"
        "    " + interpHelperName + "(wavelength, &bin, &fraction);\n"
        "    \n"
            "    return mix(convert_float(" + dataName + "[bin])  *((" + dataName + "_LARGEST_ENTRY-" + dataName + "_SMALLEST_ENTRY)/65535.f) + " + dataName + "_SMALLEST_ENTRY,\n"
            "               convert_float(" + dataName + "[bin+1])*((" + dataName + "_LARGEST_ENTRY-" + dataName + "_SMALLEST_ENTRY)/65535.f) + " + dataName + "_SMALLEST_ENTRY,\n"
            "               fraction);\n"
        "}\n"
        ;
#endif
    }
    else
    {
        funcBody = funcBody +
        "{\n"
        "    int bin; float fraction;\n"
        "    " + interpHelperName + "(wavelength, &bin, &fraction);\n"
        "    \n"
        "    return mix(" + dataName + "[bin],\n"
        "               " + dataName + "[bin+1],\n"
        "               fraction);\n"
        "}\n"
        ;
    }

    return funcDef + ";\n" + interpHelperDef + ";\n" + dataDef + interpHelperDef + interpHelperBody + funcDef + funcBody;
}

bool I3CLSimFunctionFromTable::CompareTo(const I3CLSimFunction &other) const
{
    try
    {
        const I3CLSimFunctionFromTable &other_ = dynamic_cast<const I3CLSimFunctionFromTable &>(other);
        if ((other_.equalSpacingMode_ != equalSpacingMode_) ||
            (other_.values_.size() != values_.size()))
            return false;

        for (std::size_t i=0;i<values_.size();++i)
        {
            if (other_.values_[i] != values_[i]) return false;
        }

        if (equalSpacingMode_) {
            if (other_.startWlen_ != startWlen_) return false;
            if (other_.wlenStep_ != wlenStep_) return false;
        } else {
            for (std::size_t i=0;i<wlens_.size();++i)
            {
                if (other_.wlens_[i] != wlens_[i]) return false;
            }
        }

        return true;
    }
    catch (const std::bad_cast& e)
    {
        // not of the same type, treat it as non-equal
        return false;
    }

}

I3CLSimFunctionFromTablePtr I3CLSimFunctionFromTable::Scale(double coefficient) const
{
    return I3CLSimFunctionFromTablePtr(this->ScaleImpl(coefficient));
}

I3CLSimFunctionFromTable* I3CLSimFunctionFromTable::ScaleImpl(double coefficient) const
{
    auto other = new I3CLSimFunctionFromTable(*this);
    std::transform(other->values_.begin(), other->values_.end(),
        other->values_.begin(), std::bind(std::multiplies<double>(), std::placeholders::_1, coefficient));
    return other;
}

template <class Archive>
void I3CLSimFunctionFromTable::serialize(Archive &ar, unsigned version)
{
    if (version>i3clsimfunctionfromtable_version_)
        log_fatal("Attempting to read version %u from file but running version %u of I3CLSimFunctionFromTable class.",version,i3clsimfunctionfromtable_version_);

    ar & make_nvp("I3CLSimFunction", base_object<I3CLSimFunction>(*this));
    ar & make_nvp("startWlen", startWlen_);
    ar & make_nvp("wlenStep", wlenStep_);
    ar & make_nvp("wlens", wlens_);
    ar & make_nvp("values", values_);
    ar & make_nvp("equalSpacingMode", equalSpacingMode_);
}

I3_SERIALIZABLE(I3CLSimFunctionFromTable);


/*
 * Copyright 2018-, Olivier Stasse LAAS-CNRS
 *
 */
#define EIGEN_RUNTIME_NO_MALLOC

#include <Eigen/Dense>
#include <dynamic-graph/factory.h>
#include <sot/core/debug.hh>
#include <sot/torque_control/commands-helper.hh>
#include <sot/torque_control/ddp-actuator-solver.hh>

#if DEBUG
#define ODEBUG(x) std::cout << x << std::endl
#else
#define ODEBUG(x)
#endif
#define ODEBUG3(x) std::cout << x << std::endl

#define DBGFILE "/tmp/debug-ddp_actuator_solver.dat"

#define RESETDEBUG5()                            \
  {                                              \
    std::ofstream DebugFile;                     \
    DebugFile.open(DBGFILE, std::ofstream::out); \
    DebugFile.close();                           \
  }
#define ODEBUG5FULL(x)                                                                          \
  {                                                                                \
    std::ofstream DebugFile;                                                                    \
    DebugFile.open(DBGFILE, std::ofstream::app);                                                \
    DebugFile << __FILE__ << ":" << __FUNCTION__ << "(#" << __LINE__ << "):" << x << std::endl; \
    DebugFile.close();                                                                          \
  }
#define ODEBUG5(x)                               \
  {                                              \
    std::ofstream DebugFile;                     \
    DebugFile.open(DBGFILE, std::ofstream::app); \
    DebugFile << x << std::endl;                 \
    DebugFile.close();                           \
  }

#define RESETDEBUG4()
#define ODEBUG4FULL(x)
#define ODEBUG4(x)

namespace dynamicgraph {
namespace sot {
namespace torque_control {

namespace dynamicgraph = ::dynamicgraph;
using namespace dynamicgraph;
using namespace dynamicgraph::command;
using namespace Eigen;

/// Define EntityClassName here rather than in the header file
/// so that it can be used by the macros DEFINE_SIGNAL_**_FUNCTION.
typedef DdpActuatorSolver EntityClassName;

/* --- DG FACTORY ------------------------------------------------------- */
DYNAMICGRAPH_FACTORY_ENTITY_PLUGIN(DdpActuatorSolver, "DdpActuatorSolver");

DdpActuatorSolver::DdpActuatorSolver(const std::string &name)
    : Entity(name),
      CONSTRUCT_SIGNAL_IN(pos_des, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_IN(pos_motor_measure, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_IN(pos_joint_measure, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_IN(dx_measure, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_IN(tau_measure, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_IN(tau_des, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_IN(temp_measure, dynamicgraph::Vector),
      CONSTRUCT_SIGNAL_OUT(tau, dynamicgraph::Vector, m_pos_desSIN),
      m_dt(1e-3),
      m_solver(m_model, m_cost, DISABLE_FULLDDP, DISABLE_QPBOX),
      m_T(3000),
      m_stopCrit(1e-5),
      m_iterMax(100) {
  RESETDEBUG5();
  Entity::signalRegistration(ALL_INPUT_SIGNALS << ALL_OUTPUT_SIGNALS);

  m_zeroState.setZero();

  /* Commands. */
  addCommand("init", makeCommandVoid4(*this, &DdpActuatorSolver::param_init,
                                      docCommandVoid4("Initialize the DDP solver.", "Control timestep [s].",
                                                      "Size of the preview window (in nb of samples)",
                                                      "Max. nb. of iterations", "Stopping criteria")));
}

/* --- COMMANDS ---------------------------------------------------------- */
DEFINE_SIGNAL_OUT_FUNCTION(tau, dynamicgraph::Vector) {
  /// ---- Get the information -----
  /// Desired position
  const dynamicgraph::Vector &pos_des = m_pos_desSIN(iter);
  /// Measured position
  const dynamicgraph::Vector &pos_joint_measure = m_pos_joint_measureSIN(iter);
  /// Measured speed
  const dynamicgraph::Vector &dx_measure = m_dx_measureSIN(iter);
  /// Measured temperature
  const dynamicgraph::Vector &temp_measure = m_temp_measureSIN(iter);
  /// Measured torque
  const dynamicgraph::Vector &tau_measure = m_tau_measureSIN(iter);
  /// Desired torque
  const dynamicgraph::Vector &tau_des = m_tau_desSIN(iter);

  DDPSolver<double, 5, 1>::stateVec_t xinit, xDes;

  /// --- Initialize solver ---
  xinit << pos_joint_measure(0), dx_measure(0), temp_measure(0), tau_measure(0),
      // m_ambiant_temperature;
      25.0;

  xDes << pos_des, 0.0, 25.0, tau_des, 25.0;
  ODEBUG5(xinit);
  ODEBUG5("");
  ODEBUG5(xDes);

  DCTemp model;
  CostTemp cost;
  DDPSolver<double, 5, 1> m_solver(model, cost, 0, 0);

  m_solver.FirstInitSolver(xinit, xDes, m_T, m_dt, m_iterMax, m_stopCrit);
  ODEBUG5("FirstInitSolver");

  /// --- Solve the DDP ---
  m_solver.solveTrajectory();
  ODEBUG5("Trajectory solved");

  /// --- Get the command ---
  DDPSolver<double, 5, 1>::traj lastTraj;
  lastTraj = m_solver.getLastSolvedTrajectory();
  ODEBUG5("getLastSolvedTrajectory");

  DDPSolver<double, 5, 1>::commandVecTab_t uList;
  uList = lastTraj.uList;
  ODEBUG5("uList");

  // s = uList[0];
  s.resize(32);
  s << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, uList[0], 0.0, 0.0, 0.0,
      0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
  // s.setZero(32);
  ODEBUG5(s);
  return s;
}

void DdpActuatorSolver::param_init(const double &timestep, const int &T, const int &nbItMax,
                                   const double &stopCriteria) {
  m_T = T;
  m_dt = timestep;
  m_iterMax = nbItMax;
  m_stopCrit = stopCriteria;
  m_solver.FirstInitSolver(m_zeroState, m_zeroState, m_T, m_dt, m_iterMax, m_stopCrit);
}

void DdpActuatorSolver::display(std::ostream &os) const {
  os << " T: " << m_T << " timestep: " << m_dt << " nbItMax: " << m_iterMax << " stopCriteria: " << m_stopCrit
     << std::endl;
}
}  // namespace torque_control
}  // namespace sot
}  // namespace dynamicgraph


#include "common/range_tools.hpp"

#include "customizer/cell_customizer.hpp"
#include "partitioner/multi_level_graph.hpp"
#include "partitioner/multi_level_partition.hpp"
#include "util/static_graph.hpp"

#include <boost/test/unit_test.hpp>

using namespace osrm;
using namespace osrm::customizer;
using namespace osrm::partitioner;
using namespace osrm::util;

namespace
{
struct MockEdge
{
    NodeID start;
    NodeID target;
    EdgeWeight weight;
};

auto makeGraph(const MultiLevelPartition &mlp, const std::vector<MockEdge> &mock_edges)
{
    struct EdgeData
    {
        EdgeWeight weight;
        EdgeDuration duration;
        EdgeDistance distance;
        bool forward;
        bool backward;
    };
    using Edge = static_graph_details::SortableEdgeWithData<EdgeData>;
    std::vector<Edge> edges;
    std::size_t max_id = 0;
    for (const auto &m : mock_edges)
    {
        max_id = std::max<std::size_t>(max_id, std::max(m.start, m.target));
        edges.push_back(Edge{m.start,
                             m.target,
                             m.weight,
                             2 * m.weight,
                             static_cast<EdgeDistance>(1.0),
                             true,
                             false});
        edges.push_back(Edge{m.target,
                             m.start,
                             m.weight,
                             2 * m.weight,
                             static_cast<EdgeDistance>(1.0),
                             false,
                             true});
    }
    std::sort(edges.begin(), edges.end());
    return partitioner::MultiLevelGraph<EdgeData, osrm::storage::Ownership::Container>(
        mlp, max_id + 1, edges);
}
}

BOOST_AUTO_TEST_SUITE(cell_customization_tests)

BOOST_AUTO_TEST_CASE(two_level_test)
{
    // 0 --- 1
    // |     |
    // 2 --- 3
    // node:                0  1  2  3
    std::vector<CellID> l1{{0, 0, 1, 1}};
    MultiLevelPartition mlp{{l1}, {2}};

    BOOST_REQUIRE_EQUAL(mlp.GetNumberOfLevels(), 2);

    std::vector<MockEdge> edges = {{0, 1, 1}, {0, 2, 1}, {2, 3, 1}, {3, 1, 1}, {3, 2, 1}};

    auto graph = makeGraph(mlp, edges);
    std::vector<bool> node_filter(graph.GetNumberOfNodes(), true);

    CellStorage storage(mlp, graph);
    auto metric = storage.MakeMetric();
    CellCustomizer customizer(mlp);
    CellCustomizer::Heap heap(graph.GetNumberOfNodes());

    auto cell_1_0 = storage.GetCell(metric, 1, 0);
    auto cell_1_1 = storage.GetCell(metric, 1, 1);

    REQUIRE_SIZE_RANGE(cell_1_0.GetSourceNodes(), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetDestinationNodes(), 1);
    REQUIRE_SIZE_RANGE(cell_1_1.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetDestinationNodes(), 2);

    CHECK_EQUAL_RANGE(cell_1_0.GetSourceNodes(), 0);
    CHECK_EQUAL_RANGE(cell_1_0.GetDestinationNodes(), 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetSourceNodes(), 2, 3);
    CHECK_EQUAL_RANGE(cell_1_1.GetDestinationNodes(), 2, 3);

    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(0), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(1), 0);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(1), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(0), 0);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(2), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(3), 2);

    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 0);
    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 1);

    // cell 0
    // check row source -> destination
    CHECK_EQUAL_RANGE(cell_1_0.GetOutWeight(0), 1);
    // check column destination -> source
    CHECK_EQUAL_RANGE(cell_1_0.GetInWeight(1), 1);

    // cell 1
    // check row source -> destination
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(2), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(3), 1, 0);
    // check column destination -> source
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(2), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(3), 1, 0);
}

BOOST_AUTO_TEST_CASE(two_level_test2)
{
    // 0 --- 1
    // |     |
    // 2 --- 3
    // node:                0  1  2  3
    std::vector<CellID> l1{{0, 0, 1, 1}};
    MultiLevelPartition mlp{{l1}, {2}};

    BOOST_REQUIRE_EQUAL(mlp.GetNumberOfLevels(), 2);

    std::vector<MockEdge> edges = {{0, 1, 1}, {0, 2, 1}, {2, 3, 1}, {3, 1, 1}, {3, 2, 1}};

    auto graph = makeGraph(mlp, edges);
    std::vector<bool> node_filter(graph.GetNumberOfNodes(), true);

    CellStorage storage(mlp, graph);
    auto metric = storage.MakeMetric();
    CellCustomizer customizer(mlp);
    CellCustomizer::Heap heap(graph.GetNumberOfNodes());

    auto cell_1_0 = storage.GetCell(metric, 1, 0);
    auto cell_1_1 = storage.GetCell(metric, 1, 1);

    REQUIRE_SIZE_RANGE(cell_1_0.GetSourceNodes(), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetDestinationNodes(), 1);
    REQUIRE_SIZE_RANGE(cell_1_1.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetDestinationNodes(), 2);

    CHECK_EQUAL_RANGE(cell_1_0.GetSourceNodes(), 0);
    CHECK_EQUAL_RANGE(cell_1_0.GetDestinationNodes(), 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetSourceNodes(), 2, 3);
    CHECK_EQUAL_RANGE(cell_1_1.GetDestinationNodes(), 2, 3);

    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(0), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(1), 0);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(1), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(0), 0);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(2), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(3), 2);

    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 0);
    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 1);

    // cell 0
    // check row source -> destination
    CHECK_EQUAL_RANGE(cell_1_0.GetOutWeight(0), 1);
    // check column destination -> source
    CHECK_EQUAL_RANGE(cell_1_0.GetInWeight(1), 1);

    // cell 1
    // check row source -> destination
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(2), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(3), 1, 0);
    // check column destination -> source
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(2), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(3), 1, 0);
}

BOOST_AUTO_TEST_CASE(four_levels_test)
{
    // node:                0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
    std::vector<CellID> l1{{0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3}};
    std::vector<CellID> l2{{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}};
    std::vector<CellID> l3{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
    MultiLevelPartition mlp{{l1, l2, l3}, {4, 2, 1}};

    BOOST_REQUIRE_EQUAL(mlp.GetNumberOfLevels(), 4);

    std::vector<MockEdge> edges = {
        {0, 1, 1}, // cell (0, 0, 0)
        {0, 2, 1},    {3, 1, 1},   {3, 2, 1},

        {4, 5, 1}, // cell (1, 0, 0)
        {4, 6, 1},    {4, 7, 1},   {5, 4, 1}, {5, 6, 1}, {5, 7, 1}, {6, 4, 1},
        {6, 5, 1},    {6, 7, 1},   {7, 4, 1}, {7, 5, 1}, {7, 6, 1},

        {9, 11, 1}, // cell (2, 1, 0)
        {10, 8, 1},   {11, 10, 1},

        {13, 12, 10}, // cell (3, 1, 0)
        {15, 14, 1},

        {2, 4, 1},  // edge between cells (0, 0, 0) -> (1, 0, 0)
        {5, 12, 1}, // edge between cells (1, 0, 0) -> (3, 1, 0)
        {8, 3, 1},  // edge between cells (2, 1, 0) -> (0, 0, 0)
        {9, 3, 1},  // edge between cells (2, 1, 0) -> (0, 0, 0)
        {12, 5, 1}, // edge between cells (3, 1, 0) -> (1, 0, 0)
        {13, 7, 1}, // edge between cells (3, 1, 0) -> (1, 0, 0)
        {14, 9, 1}, // edge between cells (2, 1, 0) -> (0, 0, 0)
        {14, 11, 1} // edge between cells (2, 1, 0) -> (0, 0, 0)
    };

    auto graph = makeGraph(mlp, edges);
    std::vector<bool> node_filter(true, graph.GetNumberOfNodes());

    CellStorage storage(mlp, graph);
    auto metric = storage.MakeMetric();

    auto cell_1_0 = storage.GetCell(metric, 1, 0);
    auto cell_1_1 = storage.GetCell(metric, 1, 1);
    auto cell_1_2 = storage.GetCell(metric, 1, 2);
    auto cell_1_3 = storage.GetCell(metric, 1, 3);
    auto cell_2_0 = storage.GetCell(metric, 2, 0);
    auto cell_2_1 = storage.GetCell(metric, 2, 1);
    auto cell_3_0 = storage.GetCell(metric, 3, 0);

    REQUIRE_SIZE_RANGE(cell_1_0.GetSourceNodes(), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetDestinationNodes(), 1);
    CHECK_EQUAL_RANGE(cell_1_0.GetSourceNodes(), 3);
    CHECK_EQUAL_RANGE(cell_1_0.GetDestinationNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(3), 1);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(2), 1);

    REQUIRE_SIZE_RANGE(cell_1_1.GetSourceNodes(), 3);
    REQUIRE_SIZE_RANGE(cell_1_1.GetDestinationNodes(), 3);
    CHECK_EQUAL_RANGE(cell_1_1.GetSourceNodes(), 4, 5, 7);
    CHECK_EQUAL_RANGE(cell_1_1.GetDestinationNodes(), 4, 5, 7);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(4), 3);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(5), 3);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(7), 3);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(4), 3);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(5), 3);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(7), 3);

    REQUIRE_SIZE_RANGE(cell_1_2.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetDestinationNodes(), 2);
    CHECK_EQUAL_RANGE(cell_1_2.GetSourceNodes(), 9, 11);
    CHECK_EQUAL_RANGE(cell_1_2.GetDestinationNodes(), 8, 11);
    REQUIRE_SIZE_RANGE(cell_1_2.GetOutWeight(9), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetOutWeight(11), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetInWeight(8), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetInWeight(11), 2);

    REQUIRE_SIZE_RANGE(cell_1_3.GetSourceNodes(), 1);
    REQUIRE_SIZE_RANGE(cell_1_3.GetDestinationNodes(), 2);
    CHECK_EQUAL_RANGE(cell_1_3.GetSourceNodes(), 13);
    CHECK_EQUAL_RANGE(cell_1_3.GetDestinationNodes(), 12, 14);
    REQUIRE_SIZE_RANGE(cell_1_3.GetOutWeight(13), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetInWeight(12), 1);
    REQUIRE_SIZE_RANGE(cell_1_3.GetInWeight(14), 1);

    REQUIRE_SIZE_RANGE(cell_2_0.GetSourceNodes(), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetDestinationNodes(), 2);
    CHECK_EQUAL_RANGE(cell_2_0.GetSourceNodes(), 3, 5, 7);
    CHECK_EQUAL_RANGE(cell_2_0.GetDestinationNodes(), 5, 7);
    REQUIRE_SIZE_RANGE(cell_2_0.GetOutWeight(3), 2);
    REQUIRE_SIZE_RANGE(cell_2_0.GetOutWeight(5), 2);
    REQUIRE_SIZE_RANGE(cell_2_0.GetOutWeight(7), 2);
    REQUIRE_SIZE_RANGE(cell_2_0.GetInWeight(5), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetInWeight(7), 3);

    REQUIRE_SIZE_RANGE(cell_2_1.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetDestinationNodes(), 3);
    CHECK_EQUAL_RANGE(cell_2_1.GetSourceNodes(), 9, 13);
    CHECK_EQUAL_RANGE(cell_2_1.GetDestinationNodes(), 8, 9, 12);
    REQUIRE_SIZE_RANGE(cell_2_1.GetOutWeight(9), 3);
    REQUIRE_SIZE_RANGE(cell_2_1.GetOutWeight(13), 3);
    REQUIRE_SIZE_RANGE(cell_2_1.GetInWeight(8), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetInWeight(9), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetInWeight(12), 2);

    REQUIRE_SIZE_RANGE(cell_3_0.GetSourceNodes(), 0);
    REQUIRE_SIZE_RANGE(cell_3_0.GetDestinationNodes(), 0);

    CellCustomizer customizer(mlp);
    CellCustomizer::Heap heap(graph.GetNumberOfNodes());

    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 0);
    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 1);
    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 2);
    customizer.Customize(graph, heap, storage, node_filter, metric, 1, 3);

    customizer.Customize(graph, heap, storage, node_filter, metric, 2, 0);
    customizer.Customize(graph, heap, storage, node_filter, metric, 2, 1);

    // level 1
    // cell 0
    CHECK_EQUAL_RANGE(cell_1_0.GetOutWeight(3), 1);
    CHECK_EQUAL_RANGE(cell_1_0.GetInWeight(2), 1);

    // cell 1
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(4), 0, 1, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(5), 1, 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(7), 1, 1, 0);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(4), 0, 1, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(5), 1, 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(7), 1, 1, 0);

    // cell 2
    CHECK_EQUAL_RANGE(cell_1_2.GetOutWeight(9), 3, 1);
    CHECK_EQUAL_RANGE(cell_1_2.GetOutWeight(11), 2, 0);
    CHECK_EQUAL_RANGE(cell_1_2.GetInWeight(8), 3, 2);
    CHECK_EQUAL_RANGE(cell_1_2.GetInWeight(11), 1, 0);

    // cell 3
    CHECK_EQUAL_RANGE(cell_1_3.GetOutWeight(13), 10, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_3.GetInWeight(12), 10);
    CHECK_EQUAL_RANGE(cell_1_3.GetInWeight(14), INVALID_EDGE_WEIGHT);

    // level 2
    // cell 0
    CHECK_EQUAL_RANGE(cell_2_0.GetOutWeight(3), 3, 3);
    CHECK_EQUAL_RANGE(cell_2_0.GetOutWeight(5), 0, 1);
    CHECK_EQUAL_RANGE(cell_2_0.GetOutWeight(7), 1, 0);
    CHECK_EQUAL_RANGE(cell_2_0.GetInWeight(5), 3, 0, 1);
    CHECK_EQUAL_RANGE(cell_2_0.GetInWeight(7), 3, 1, 0);

    // cell 1
    CHECK_EQUAL_RANGE(cell_2_1.GetOutWeight(9), 3, 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_1.GetOutWeight(13), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT, 10);
    CHECK_EQUAL_RANGE(cell_2_1.GetInWeight(8), 3, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_1.GetInWeight(9), 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_1.GetInWeight(12), INVALID_EDGE_WEIGHT, 10);

    CHECK_EQUAL_RANGE(cell_2_1.GetOutDuration(9), 6, 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_1.GetOutDuration(13), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT, 20);
    CHECK_EQUAL_RANGE(cell_2_1.GetInDuration(8), 6, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_1.GetInDuration(9), 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_1.GetInDuration(12), INVALID_EDGE_WEIGHT, 20);

    CellStorage storage_rec(mlp, graph);
    auto metric_rec = storage_rec.MakeMetric();
    CellUpdateRecord cell_update_record(mlp, false);
    customizer.Customize(graph, storage_rec, node_filter, metric_rec, cell_update_record);

    CHECK_EQUAL_COLLECTIONS(cell_2_1.GetOutWeight(9),
                            storage_rec.GetCell(metric_rec, 2, 1).GetOutWeight(9));
    CHECK_EQUAL_COLLECTIONS(cell_2_1.GetOutWeight(13),
                            storage_rec.GetCell(metric_rec, 2, 1).GetOutWeight(13));
    CHECK_EQUAL_COLLECTIONS(cell_2_1.GetInWeight(8),
                            storage_rec.GetCell(metric_rec, 2, 1).GetInWeight(8));
    CHECK_EQUAL_COLLECTIONS(cell_2_1.GetInWeight(9),
                            storage_rec.GetCell(metric_rec, 2, 1).GetInWeight(9));
    CHECK_EQUAL_COLLECTIONS(cell_2_1.GetInWeight(12),
                            storage_rec.GetCell(metric_rec, 2, 1).GetInWeight(12));
}

BOOST_AUTO_TEST_CASE(exclude_test)
{
    // 0 --- 1 --- 5 --- 6
    // |  /  |     |     |
    // 2 ----3 --- 4 --- 7
    // \__________/
    std::vector<MockEdge> edges = {
        {0, 1, 1}, {0, 2, 1}, {1, 0, 1}, {1, 2, 10}, {1, 3, 1}, {1, 5, 1}, {2, 0, 1}, {2, 1, 10},
        {2, 3, 1}, {2, 4, 1}, {3, 1, 1}, {3, 2, 1},  {3, 4, 1}, {4, 2, 1}, {4, 3, 1}, {4, 5, 1},
        {4, 7, 1}, {5, 1, 1}, {5, 4, 1}, {5, 6, 1},  {6, 5, 1}, {6, 7, 1}, {7, 4, 1}, {7, 6, 1},
    };

    // node:                0  1  2  3  4  5  6  7
    std::vector<CellID> l1{{0, 0, 1, 1, 3, 2, 2, 3}};
    std::vector<CellID> l2{{0, 0, 0, 0, 1, 1, 1, 1}};
    std::vector<CellID> l3{{0, 0, 0, 0, 0, 0, 0, 0}};
    MultiLevelPartition mlp{{l1, l2, l3}, {4, 2, 1}};

    BOOST_REQUIRE_EQUAL(mlp.GetNumberOfLevels(), 4);

    auto graph = makeGraph(mlp, edges);
    // exclude node 0, 3 and 7
    std::vector<bool> node_filter = {false, true, true, false, true, true, true, false};

    CellCustomizer customizer(mlp);
    CellStorage storage(mlp, graph);
    auto metric = storage.MakeMetric();
    CellUpdateRecord cell_update_record(mlp, false);
    customizer.Customize(graph, storage, node_filter, metric, cell_update_record);

    auto cell_1_0 = storage.GetCell(metric, 1, 0);
    auto cell_1_1 = storage.GetCell(metric, 1, 1);
    auto cell_1_2 = storage.GetCell(metric, 1, 2);
    auto cell_1_3 = storage.GetCell(metric, 1, 3);

    REQUIRE_SIZE_RANGE(cell_1_0.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_0.GetDestinationNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetDestinationNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetDestinationNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetDestinationNodes(), 2);

    CHECK_EQUAL_RANGE(cell_1_0.GetSourceNodes(), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_0.GetDestinationNodes(), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_1.GetSourceNodes(), 2, 3);
    CHECK_EQUAL_RANGE(cell_1_1.GetDestinationNodes(), 2, 3);
    CHECK_EQUAL_RANGE(cell_1_2.GetSourceNodes(), 5, 6);
    CHECK_EQUAL_RANGE(cell_1_2.GetDestinationNodes(), 5, 6);
    CHECK_EQUAL_RANGE(cell_1_3.GetSourceNodes(), 4, 7);
    CHECK_EQUAL_RANGE(cell_1_3.GetDestinationNodes(), 4, 7);

    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(0), 2);
    REQUIRE_SIZE_RANGE(cell_1_0.GetOutWeight(1), 2);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(0), 2);
    REQUIRE_SIZE_RANGE(cell_1_0.GetInWeight(1), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(2), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetOutWeight(3), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(2), 2);
    REQUIRE_SIZE_RANGE(cell_1_1.GetInWeight(3), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetOutWeight(5), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetOutWeight(6), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetInWeight(5), 2);
    REQUIRE_SIZE_RANGE(cell_1_2.GetInWeight(6), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetOutWeight(4), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetOutWeight(7), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetInWeight(4), 2);
    REQUIRE_SIZE_RANGE(cell_1_3.GetInWeight(7), 2);

    CHECK_EQUAL_RANGE(cell_1_0.GetOutWeight(0), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_0.GetOutWeight(1), INVALID_EDGE_WEIGHT, 0);
    CHECK_EQUAL_RANGE(cell_1_0.GetInWeight(0), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_0.GetInWeight(1), INVALID_EDGE_WEIGHT, 0);
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(2), 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_1.GetOutWeight(3), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(2), 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_1.GetInWeight(3), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_2.GetOutWeight(5), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_2.GetOutWeight(6), 1, 0);
    CHECK_EQUAL_RANGE(cell_1_2.GetInWeight(5), 0, 1);
    CHECK_EQUAL_RANGE(cell_1_2.GetInWeight(6), 1, 0);
    CHECK_EQUAL_RANGE(cell_1_3.GetOutWeight(4), 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_3.GetOutWeight(7), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_3.GetInWeight(4), 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_1_3.GetInWeight(7), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);

    auto cell_2_0 = storage.GetCell(metric, 2, 0);
    auto cell_2_1 = storage.GetCell(metric, 2, 1);

    REQUIRE_SIZE_RANGE(cell_2_0.GetSourceNodes(), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetDestinationNodes(), 3);
    REQUIRE_SIZE_RANGE(cell_2_1.GetSourceNodes(), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetDestinationNodes(), 2);

    CHECK_EQUAL_RANGE(cell_2_0.GetSourceNodes(), 1, 2, 3);
    CHECK_EQUAL_RANGE(cell_2_0.GetDestinationNodes(), 1, 2, 3);
    CHECK_EQUAL_RANGE(cell_2_1.GetSourceNodes(), 4, 5);
    CHECK_EQUAL_RANGE(cell_2_1.GetDestinationNodes(), 4, 5);

    REQUIRE_SIZE_RANGE(cell_2_0.GetOutWeight(1), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetOutWeight(2), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetOutWeight(3), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetInWeight(1), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetInWeight(2), 3);
    REQUIRE_SIZE_RANGE(cell_2_0.GetInWeight(3), 3);
    REQUIRE_SIZE_RANGE(cell_2_1.GetOutWeight(4), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetOutWeight(5), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetInWeight(4), 2);
    REQUIRE_SIZE_RANGE(cell_2_1.GetInWeight(5), 2);

    CHECK_EQUAL_RANGE(cell_2_0.GetOutWeight(1), 0, 10, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_0.GetOutWeight(2), 10, 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(
        cell_2_0.GetOutWeight(3), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_0.GetInWeight(1), 0, 10, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(cell_2_0.GetInWeight(2), 10, 0, INVALID_EDGE_WEIGHT);
    CHECK_EQUAL_RANGE(
        cell_2_0.GetInWeight(3), INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT, INVALID_EDGE_WEIGHT);

    CHECK_EQUAL_RANGE(cell_2_1.GetOutWeight(4), 0, 1);
    CHECK_EQUAL_RANGE(cell_2_1.GetOutWeight(5), 1, 0);
    CHECK_EQUAL_RANGE(cell_2_1.GetInWeight(4), 0, 1);
    CHECK_EQUAL_RANGE(cell_2_1.GetInWeight(5), 1, 0);
}

BOOST_AUTO_TEST_SUITE_END()


/*
 Copyright (C) 2016 Quaternion Risk Management Ltd
 All rights reserved.

 This file is part of ORE, a free-software/open-source library
 for transparent pricing and risk analysis - http://opensourcerisk.org

 ORE is free software: you can redistribute it and/or modify it
 under the terms of the Modified BSD License.  You should have received a
 copy of the license along with this program.
 The license is also available online at <http://opensourcerisk.org>

 This program is distributed on the basis that it will form a useful
 contribution to risk analytics and model standardisation, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
*/

#include <boost/test/unit_test.hpp>
#include <orea/scenario/scenariosimmarket.hpp>
#include <orea/scenario/scenariosimmarketparameters.hpp>
#include <ored/configuration/conventions.hpp>
#include <ored/marketdata/market.hpp>
#include <ored/marketdata/marketimpl.hpp>
#include <ored/utilities/log.hpp>
#include <oret/toplevelfixture.hpp>
#include <ql/termstructures/credit/flathazardrate.hpp>
#include <ql/termstructures/volatility/capfloor/constantcapfloortermvol.hpp>
#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
#include <ql/termstructures/volatility/swaption/swaptionconstantvol.hpp>
#include <ql/termstructures/yield/flatforward.hpp>
#include <ql/time/daycounters/actualactual.hpp>
#include <test/testmarket.hpp>

#include <ql/indexes/ibor/all.hpp>

using namespace QuantLib;
using namespace QuantExt;
using namespace boost::unit_test_framework;
using namespace std;
using namespace ore;
using namespace ore::data;

using testsuite::TestMarket;

namespace {

boost::shared_ptr<data::Conventions> convs() {
    boost::shared_ptr<data::Conventions> conventions(new data::Conventions());

    boost::shared_ptr<data::Convention> swapIndexConv(
        new data::SwapIndexConvention("EUR-CMS-2Y", "EUR-6M-SWAP-CONVENTIONS"));
    conventions->add(swapIndexConv);

    boost::shared_ptr<data::Convention> swapConv(
        new data::IRSwapConvention("EUR-6M-SWAP-CONVENTIONS", "TARGET", "Annual", "MF", "30/360", "EUR-EURIBOR-6M"));
    conventions->add(swapConv);

    return conventions;
}

boost::shared_ptr<analytics::ScenarioSimMarketParameters> scenarioParameters() {
    boost::shared_ptr<analytics::ScenarioSimMarketParameters> parameters(new analytics::ScenarioSimMarketParameters());
    parameters->baseCcy() = "EUR";
    parameters->ccys() = {"EUR", "USD"};
    parameters->setYieldCurveTenors("", {6 * Months, 1 * Years, 2 * Years});
    parameters->indices() = {"EUR-EURIBOR-6M", "USD-LIBOR-6M"};
    parameters->interpolation() = "LogLinear";
    parameters->extrapolate() = true;
    parameters->setYieldCurveDayCounters("", "ACT/ACT");

    parameters->swapVolTerms() = {6 * Months, 1 * Years};
    parameters->swapVolExpiries() = {1 * Years, 2 * Years};
    parameters->swapVolCcys() = {"EUR", "USD"};
    parameters->swapVolDecayMode() = "ForwardVariance";
    parameters->setSwapVolDayCounters("", "ACT/ACT");

    parameters->defaultNames() = {"dc2"};
    parameters->setDefaultTenors("", {6 * Months, 8 * Months, 1 * Years, 2 * Years});
    parameters->setDefaultCurveDayCounters("", "ACT/ACT");

    parameters->simulateFXVols() = false;
    parameters->fxVolExpiries() = {2 * Years, 3 * Years, 4 * Years};
    parameters->fxVolDecayMode() = "ConstantVariance";
    parameters->simulateEquityVols() = false;
    parameters->setFxVolDayCounters("", "ACT/ACT");

    parameters->fxVolCcyPairs() = {"USDEUR"};

    parameters->fxCcyPairs() = {"USDEUR"};

    parameters->zeroInflationIndices() = {"EUHICPXT"};
    parameters->setZeroInflationTenors("", {6 * Months, 1 * Years, 2 * Years});
    parameters->setZeroInflationDayCounters("", "ACT/ACT");

    parameters->simulateCorrelations() = false;
    parameters->correlationExpiries() = {1 * Years, 2 * Years};
    parameters->correlationPairs() = {make_pair("EUR-CMS-10Y", "EUR-CMS-1Y"), make_pair("USD-CMS-10Y", "USD-CMS-1Y")};
    return parameters;
}
} // namespace

void testFxSpot(boost::shared_ptr<ore::data::Market>& initMarket,
                boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {

    for (const auto& ccy : parameters->ccys()) {
        if (ccy != parameters->baseCcy()) {
            string ccyPair(parameters->baseCcy() + ccy);
            string ccyReverse(ccy + parameters->baseCcy());

            Handle<Quote> fx_sim = simMarket->fxSpot(ccyPair);
            Handle<Quote> fx_init = initMarket->fxSpot(ccyPair);
            if (fx_sim.empty())
                BOOST_FAIL("fx_sim handle is empty");
            if (fx_init.empty())
                BOOST_FAIL("fx_init handle is empty");

            BOOST_CHECK_CLOSE(fx_init->value(), fx_sim->value(), 1e-12);

            fx_sim = simMarket->fxSpot(ccyReverse);
            fx_init = initMarket->fxSpot(ccyReverse);
            if (fx_sim.empty())
                BOOST_FAIL("fx_sim handle is empty");
            if (fx_init.empty())
                BOOST_FAIL("fx_init handle is empty");

            BOOST_CHECK_CLOSE(fx_init->value(), fx_sim->value(), 1e-12);
        }
    }
}

void testDiscountCurve(boost::shared_ptr<ore::data::Market>& initMarket,
                       boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                       boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {

    for (const auto& ccy : parameters->ccys()) {
        Handle<YieldTermStructure> simCurve = simMarket->discountCurve(ccy);
        Handle<YieldTermStructure> initCurve = initMarket->discountCurve(ccy);

        BOOST_CHECK_CLOSE(simCurve->discount(0.5), initCurve->discount(0.5), 1e-12);
    }
}

void testIndexCurve(boost::shared_ptr<ore::data::Market>& initMarket,
                    boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                    boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {

    for (const auto& ind : parameters->indices()) {
        Handle<YieldTermStructure> simCurve = simMarket->iborIndex(ind)->forwardingTermStructure();
        Handle<YieldTermStructure> initCurve = initMarket->iborIndex(ind)->forwardingTermStructure();

        BOOST_CHECK_CLOSE(simCurve->discount(1), initCurve->discount(1), 1e-4);
    }
}

void testSwaptionVolCurve(boost::shared_ptr<ore::data::Market>& initMarket,
                          boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                          boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {
    for (const auto& ccy : parameters->ccys()) {
        Handle<QuantLib::SwaptionVolatilityStructure> simCurve = simMarket->swaptionVol(ccy);
        Handle<QuantLib::SwaptionVolatilityStructure> initCurve = initMarket->swaptionVol(ccy);
        for (const auto& maturity : parameters->swapVolExpiries()) {
            for (const auto& tenor : parameters->swapVolTerms()) {
                BOOST_CHECK_CLOSE(simCurve->volatility(maturity, tenor, 0.0, true),
                                  initCurve->volatility(maturity, tenor, 0.0, true), 1e-12);
            }
        }
    }
}

void testFxVolCurve(boost::shared_ptr<data::Market>& initMarket,
                    boost::shared_ptr<analytics::ScenarioSimMarket>& simMarket,
                    boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {
    for (const auto& ccyPair : parameters->fxVolCcyPairs()) {
        Handle<BlackVolTermStructure> simCurve = simMarket->fxVol(ccyPair);
        Handle<BlackVolTermStructure> initCurve = initMarket->fxVol(ccyPair);
        vector<Date> dates;
        Date asof = initMarket->asofDate();
        for (Size i = 0; i < parameters->fxVolExpiries().size(); i++) {
            dates.push_back(asof + parameters->fxVolExpiries()[i]);
        }

        for (const auto& date : dates) {
            BOOST_CHECK_CLOSE(simCurve->blackVol(date, 0.0, true), initCurve->blackVol(date, 0.0, true), 1e-12);
        }
    }
}

void testDefaultCurve(boost::shared_ptr<ore::data::Market>& initMarket,
                      boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                      boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {
    for (const auto& spec : parameters->defaultNames()) {

        Handle<DefaultProbabilityTermStructure> simCurve = simMarket->defaultCurve(spec);
        Handle<DefaultProbabilityTermStructure> initCurve = initMarket->defaultCurve(spec);
        BOOST_CHECK_EQUAL(initCurve->referenceDate(), simCurve->referenceDate());
        vector<Date> dates;
        Date asof = initMarket->asofDate();
        for (Size i = 0; i < parameters->defaultTenors("").size(); i++) {
            dates.push_back(asof + parameters->defaultTenors("")[i]);
        }

        for (const auto& date : dates) {
            BOOST_CHECK_CLOSE(simCurve->survivalProbability(date, true), initCurve->survivalProbability(date, true),
                              1e-12);
        }
    }
}

void testZeroInflationCurve(boost::shared_ptr<ore::data::Market>& initMarket,
                            boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                            boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {
    for (const auto& spec : parameters->zeroInflationIndices()) {

        Handle<ZeroInflationTermStructure> simCurve = simMarket->zeroInflationIndex(spec)->zeroInflationTermStructure();
        Handle<ZeroInflationTermStructure> initCurve =
            initMarket->zeroInflationIndex(spec)->zeroInflationTermStructure();
        BOOST_CHECK_EQUAL(initCurve->referenceDate(), simCurve->referenceDate());
        vector<Date> dates;
        Date asof = initMarket->asofDate();
        for (Size i = 0; i < parameters->zeroInflationTenors("").size(); i++) {
            dates.push_back(asof + parameters->zeroInflationTenors("")[i]);
        }

        for (const auto& date : dates) {
            BOOST_CHECK_CLOSE(simCurve->zeroRate(date), initCurve->zeroRate(date), 1e-12);
        }
    }
}

void testCorrelationCurve(boost::shared_ptr<ore::data::Market>& initMarket,
                          boost::shared_ptr<ore::analytics::ScenarioSimMarket>& simMarket,
                          boost::shared_ptr<analytics::ScenarioSimMarketParameters>& parameters) {
    for (const auto& spec : parameters->correlationPairs()) {
        Handle<QuantExt::CorrelationTermStructure> simCurve = simMarket->correlationCurve(spec.first, spec.second);
        Handle<QuantExt::CorrelationTermStructure> initCurve = initMarket->correlationCurve(spec.first, spec.second);
        BOOST_CHECK_EQUAL(initCurve->referenceDate(), simCurve->referenceDate());
        vector<Date> dates;
        Date asof = initMarket->asofDate();
        for (Size i = 0; i < parameters->correlationExpiries().size(); i++) {
            dates.push_back(asof + parameters->correlationExpiries()[i]);
        }

        for (const auto& date : dates) {
            BOOST_CHECK_CLOSE(simCurve->correlation(date), initCurve->correlation(date), 1e-12);
        }
    }
}

void testToXML(boost::shared_ptr<analytics::ScenarioSimMarketParameters> params) {

    BOOST_TEST_MESSAGE("Testing to XML...");
    XMLDocument outDoc;
    string testFile = "simtest.xml";
    XMLNode* node = params->toXML(outDoc);

    XMLNode* simulationNode = outDoc.allocNode("Simulation");
    outDoc.appendNode(simulationNode);
    XMLUtils::appendNode(simulationNode, node);
    outDoc.toFile(testFile);

    boost::shared_ptr<analytics::ScenarioSimMarketParameters> newParams(new analytics::ScenarioSimMarketParameters());
    newParams->fromFile(testFile);
    BOOST_CHECK(*params == *newParams);

    newParams->baseCcy() = "JPY";
    BOOST_CHECK(*params != *newParams);

    remove("simtest.xml");
}

BOOST_FIXTURE_TEST_SUITE(OREAnalyticsTestSuite, ore::test::TopLevelFixture)

BOOST_AUTO_TEST_SUITE(ScenarioSimMarketTest)

BOOST_AUTO_TEST_CASE(testScenarioSimMarket) {
    BOOST_TEST_MESSAGE("Testing OREAnalytics ScenarioSimMarket...");

    SavedSettings backup;

    Date today(20, Jan, 2015);
    Settings::instance().evaluationDate() = today;
    boost::shared_ptr<ore::data::Market> initMarket = boost::make_shared<TestMarket>(today);

    // Empty scenario generator
    boost::shared_ptr<ore::analytics::ScenarioGenerator> scenarioGenerator;

    // build scenario
    boost::shared_ptr<analytics::ScenarioSimMarketParameters> parameters = scenarioParameters();
    Conventions conventions = *convs();
    // build scenario sim market
    boost::shared_ptr<analytics::ScenarioSimMarket> simMarket(
        new analytics::ScenarioSimMarket(initMarket, parameters, conventions));
    simMarket->scenarioGenerator() = scenarioGenerator;

    // test
    testFxSpot(initMarket, simMarket, parameters);
    testDiscountCurve(initMarket, simMarket, parameters);
    testIndexCurve(initMarket, simMarket, parameters);
    testSwaptionVolCurve(initMarket, simMarket, parameters);
    testFxVolCurve(initMarket, simMarket, parameters);
    testDefaultCurve(initMarket, simMarket, parameters);
    testZeroInflationCurve(initMarket, simMarket, parameters);
    testCorrelationCurve(initMarket, simMarket, parameters);
    testToXML(parameters);
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()


#include "datatypes/raster_operations.h"
#include "util/raster_calculations.h"
#include "util/parsing.h"
#include "util/benchmark.h"
#include "gdal_source.h"

#include <boost/filesystem.hpp>
#include <fstream>
#include <ctime>
#include <gdal.h>

using namespace rts;
using namespace boost::posix_time;

template<class T>
struct GdalSourceWriter {
    static void rasterOperation(TypedRaster<T> *raster, std::shared_ptr<GDALDataset> rasterDataset,
                                GDALRasterBand *rasterBand, const Descriptor &self,
                                Resolution fill_from, Resolution res_left_to_fill)
    {
        Resolution tileRes = raster->getResolution();

        //calculate where the qrect is in source raster pixels. see mappings gdalsource.
        double adfGeoTransform[6];
        if(rasterDataset->GetGeoTransform( adfGeoTransform ) != CE_None ) {
            throw std::runtime_error("GDAL Source: No GeoTransform information in raster");
        }

        double origin_x = adfGeoTransform[0];
        double origin_y = adfGeoTransform[3];
        double scale_x = adfGeoTransform[1];
        double scale_y = adfGeoTransform[5];

        int rasterSizeX = rasterBand->GetXSize();
        int rasterSizeY = rasterBand->GetYSize();

        //GDAL often has a positive y origin and a negative scale, but I assume the origin to be the smaller
        //coordinate and the scale to be positive, so swap that here. use raster size for calculation of actual origin.
        if(scale_y < 0){
            double old_origin_y = origin_y;
            origin_y = origin_y + scale_y * rasterSizeY;
            scale_y *= -1;
        }

        SpatialReference spatInfo = self.tileSpatialInfo;
        if(spatInfo.x1 < self.rasterInfo.x1)
            spatInfo.x1 = self.rasterInfo.x1;
        if(spatInfo.x2 > self.rasterInfo.x2)
            spatInfo.x2 = self.rasterInfo.x2;
        if(spatInfo.y1 < self.rasterInfo.y1)
            spatInfo.y1 = self.rasterInfo.y1;
        if(spatInfo.y2 > self.rasterInfo.y2)
            spatInfo.y2 = self.rasterInfo.y2;

        int pixel_x1 = 0;
        int pixel_y1 = 0;
        int pixel_x2 = rasterSizeX;
        int pixel_y2 = rasterSizeY;
        int pixel_width = rasterSizeX;
        int pixel_height = rasterSizeY;

        pixel_x1 = static_cast<int>(floor((spatInfo.x1 - origin_x) / scale_x));
        pixel_y1 = static_cast<int>(floor((spatInfo.y1 - origin_y) / scale_y));
        pixel_x2 = static_cast<int>(floor((spatInfo.x2 - origin_x) / scale_x));
        pixel_y2 = static_cast<int>(floor((spatInfo.y2 - origin_y) / scale_y));

        if (pixel_x1 > pixel_x2)
            std::swap(pixel_x1, pixel_x2);
        if (pixel_y1 > pixel_y2)
            std::swap(pixel_y1, pixel_y2);

        pixel_width = pixel_x2 - pixel_x1;
        pixel_height = pixel_y2 - pixel_y1;

        int gdal_pixel_x1 = std::min(rasterSizeX, std::max(0, pixel_x1));
        int gdal_pixel_y1 = std::min(rasterSizeY, std::max(0, pixel_y1));

        int gdal_pixel_x2 = std::min(rasterSizeX, std::max(0, pixel_x2));
        int gdal_pixel_y2 = std::min(rasterSizeY, std::max(0, pixel_y2));

        int gdal_pixel_width = gdal_pixel_x2 - gdal_pixel_x1;
        int gdal_pixel_height = gdal_pixel_y2 - gdal_pixel_y1;

        Resolution size = tileRes - fill_from;
        if(res_left_to_fill.resX < size.resX)
            size.resX = res_left_to_fill.resX - fill_from.resX;
        if(res_left_to_fill.resY < size.resY)
            size.resY = res_left_to_fill.resY - fill_from.resY;

        if(fill_from.resX > 0 || fill_from.resY > 0 || size.resX < tileRes.resX || size.resY < tileRes.resY){
            for (int x = 0; x < tileRes.resX; ++x) {
                for (int y = 0; y < tileRes.resY; ++y) {
                    raster->setCell(x,y, (T)self.nodata);
                }
            }
        }

        void *buffer = nullptr;
        if(fill_from.resX > 0 || fill_from.resY > 0)
            buffer = raster->getVoidDataPointerOffset(fill_from.resX, fill_from.resY);
        else
            buffer = raster->getVoidDataPointer();

        auto res = rasterBand->RasterIO(GF_Read, gdal_pixel_x1, gdal_pixel_y1, gdal_pixel_width, gdal_pixel_height,
                buffer, size.resX, size.resY, self.dataType, 0, sizeof(T) * tileRes.resX, nullptr);

        if(res != CE_None){
            throw std::runtime_error("GDAL Source: Reading from raster failed.");
        }
    }
};

GDALSource::GDALSource(const QueryRectangle &qrect, const Json::Value &params)
        : SourceBackend(qrect, params), currDataset(nullptr), currRasterband(nullptr), currDatasetTime(0)
{

}

void GDALSource::initialize() {
    GDALUtil::initGdal();

    Json::Value dataset_json    = loadDatasetJson(params["dataset"].asString());
    channel                     = dataset_json["channel"].asInt();

    baseFileName                = dataset_json["filename"].asString();
    path                        = dataset_json["path"].asString();
    timeFormat                  = dataset_json["time_format"].asString();

    Json::Value time_interval_json   = dataset_json["time_interval"];
    timeInterval = TimeInterval(time_interval_json);

    datasetStartTime = (double)to_time_t(time_from_string(dataset_json["time_start"].asString()));
    datasetEndTime   = (double)to_time_t(time_from_string(dataset_json["time_end"].asString()));

    Json::Value coords = dataset_json["coords"];

    if(params["dataset"] == "msg_eu_scaled_int") {
        //TODO: bad hack for meteosat queries. test properly what the error is.
        auto extent = SpatialReference(coords["extent"]);
        origin.x = extent.x1;
        origin.y = extent.y1;
    } else {
        origin = qrect.projection.getOrigin();
    }

}

OptionalDescriptor GDALSource::createDescriptor(double time, int pixelStartX, int pixelStartY, int tileIndex, const Resolution &rasterWorldPixelStart, const Scale &scale, const Origin &origin, const Resolution &tileCount) {

    if(currDataset == nullptr || time != currDatasetTime){
        loadCurrentGdalDataset(time);
    }

    double nodata = currRasterband->GetNoDataValue();
    GDALDataType dataType = currRasterband->GetRasterDataType();

    Resolution fillFrom(0, 0);

    //fillFrom: for fixed alignment of tiles, start of a tile is not always 0, based on what pixel in world space the tile starts.
    if(pixelStartX < 0){
        fillFrom.resX = (uint32_t)(-1 * pixelStartX);
    }
    if(pixelStartY < 0){
        fillFrom.resY = (uint32_t)(-1 * pixelStartY);
    }

    //total pixel left to fill, may be bigger as tileSize
    Resolution resLeftToFill(qrect.resX - pixelStartX, qrect.resY - pixelStartY); //pixelInTileLeftToFill

    Resolution tileStartWorldRes(rasterWorldPixelStart.resX + pixelStartX, rasterWorldPixelStart.resY + pixelStartY);
    SpatialReference tileSpat = RasterCalculations::pixelToSpatialRectangle(scale, origin, tileStartWorldRes, tileStartWorldRes + qrect.tileRes);

    auto getter = [currDataset = currDataset, currRasterband = currRasterband, fillFrom = fillFrom, resLeftToFill = resLeftToFill](const Descriptor &self) -> std::unique_ptr<Raster> {
        Benchmark::startSource();
        std::unique_ptr<Raster> out = Raster::createRaster(self.dataType, self.tileResolution);
        RasterOperations::callUnary<GdalSourceWriter>(out.get(), currDataset, currRasterband, self, fillFrom, resLeftToFill);
        Benchmark::endSource();
        return out;
    };

    TemporalReference tempInfo(time, getCurrentTimeEnd(time));
    SpatialTemporalReference rasterInfo = qrect;
    rasterInfo.t1 = tempInfo.t1;
    rasterInfo.t2 = tempInfo.t2;

    return rts::make_optional<Descriptor>(std::move(getter), rasterInfo, tileSpat, qrect.tileRes,
                                          qrect.order, tileIndex, tileCount, nodata, dataType);
}

Origin GDALSource::getOrigin() const {
    return origin;
}

void GDALSource::beforeTemporalIncrease(){
    currDataset = nullptr;
}

bool GDALSource::supportsOrder(Order o) const {
    return o == Order::Temporal || o == Order::Spatial;
}

void GDALSource::increaseCurrentTime(double &currTime) {
    ptime currPTime = from_time_t((time_t)currTime);
    timeInterval.increase(currPTime);
    currTime = (double)to_time_t(currPTime);
}

double GDALSource::getCurrentTimeEnd(double currTime) const {
    ptime currPTime = from_time_t((time_t)currTime);
    timeInterval.increase(currPTime);
    return static_cast<double>(to_time_t(currPTime));
}

Json::Value GDALSource::loadDatasetJson(const std::string &name) {
    boost::filesystem::path p("../../test/data/gdal_source");
    p /= name + ".json";
    std::ifstream file_in(p.string());
    Json::Value dataset_json;
    file_in >> dataset_json;
    return dataset_json;
}

double GDALSource::parseIsoTime(const std::string &str) const {
    std::tm tm = {};
    if (strptime(str.c_str(), "%Y-%m-%dT%H:%M:%S", &tm))
        return (double)timegm(&tm);
    else
        throw std::runtime_error("Could not parse time.");
}

void GDALSource::loadCurrentGdalDataset(double time) {
    currDatasetTime = time;
    std::string timeString = GDALUtil::timeToString((time_t)time, timeFormat);

    std::string placeholder = "%%%TIME_STRING%%%";
    size_t placeholderPos = baseFileName.find(placeholder);

    std::string fileName = baseFileName;
    fileName.replace(placeholderPos, placeholder.length(), timeString);

    if(openDatasets.find(fileName) != openDatasets.end())
    {
        currDataset = openDatasets[fileName];
        currRasterband = currDataset->GetRasterBand(channel);
    }
    else
    {
        boost::filesystem::path file_path(path.c_str());
        file_path /= fileName; //i dont get how you can not append a std::filesystem::path with a std::string ?!?!

        auto dataset = (GDALDataset *)GDALOpen(file_path.c_str(), GA_ReadOnly);
        if(dataset == nullptr){
            throw std::runtime_error("GDAL dataset could not be opened: " + file_path.string());
        }
        currRasterband = dataset->GetRasterBand(channel);

        currDataset = std::shared_ptr<GDALDataset>(dataset, [](GDALDataset *d){
            GDALClose(d);
            d = nullptr;
        });
        //cache the dataset when its spatial order.
        if(qrect.order == Order::Spatial)
            openDatasets[fileName] = currDataset;
    }

}


/** \file
 *
 *  Copyright (c) 2015 by Travis Gockel. All rights reserved.
 *
 *  This program is free software: you can redistribute it and/or modify it under the terms of the Apache License
 *  as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later
 *  version.
 *
 *  \author Travis Gockel (travis@gockelhut.com)
**/
#include "core.hpp"

#include <jsonv/all.hpp>

#include <chrono>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <map>
#include <random>
#include <sstream>

#include <boost/lexical_cast.hpp>

using namespace jsonv;

struct generated_json_settings
{
    std::uniform_int_distribution<int> kind_distribution{0, 6};

    template <typename TRng>
    jsonv::kind kind(TRng& rng, std::size_t current_depth)
    {
        if (current_depth < 2)
            return jsonv::kind::object;

        auto k = static_cast<jsonv::kind>(kind_distribution(rng));
        if (current_depth > 5 && (k == jsonv::kind::array || k == jsonv::kind::object))
            return kind(rng, current_depth);
        else
            return k;
    }

    std::uniform_int_distribution<std::size_t> array_length_distribution{0, 100};
    template <typename TRng>
    std::size_t array_length(TRng& rng)
    {
        return array_length_distribution(rng);
    }

    std::uniform_int_distribution<std::size_t> object_size_distribution{5, 25};
    template <typename TRng>
    std::size_t object_size(TRng& rng)
    {
        return object_size_distribution(rng);
    }

    std::uniform_int_distribution<std::int64_t> string_length_distribution{0, 100};
    template <typename TRng>
    std::size_t string_length(TRng& rng)
    {
        return string_length_distribution(rng);
    }

    std::normal_distribution<> decimal_distribution{1.0, 40.0};
    template <typename TRng>
    double decimal(TRng& rng)
    {
        return decimal_distribution(rng);
    }

    std::uniform_int_distribution<std::int64_t> integer_distribution{~0};
    template <typename TRng>
    std::int64_t integer(TRng& rng)
    {
        return integer_distribution(rng);
    }
};

template <typename TRng, typename TSettings>
jsonv::value generate_json(TRng& rng, TSettings& settings, std::size_t current_depth = 0)
{
    switch (settings.kind(rng, current_depth))
    {
        case kind::array:
        {
            value out = array();
            std::size_t len = settings.array_length(rng);
            for (std::size_t idx = 0; idx < len; ++idx)
                out.push_back(generate_json(rng, settings, current_depth + 1));
            return out;
        }
        case kind::boolean:
            return !(rng() & 1);
        case kind::decimal:
            return settings.decimal(rng);
        case kind::integer:
            return settings.integer(rng);
        case kind::string:
            // TODO: Improve this
            return std::string(settings.string_length(rng), 'a');
        case kind::object:
        {
            value out = object();
            std::size_t len = settings.object_size(rng);
            for (std::size_t idx = 1; idx <= len; ++idx)
            {
                // TODO: Improve
                std::string key(idx, 'a');
                value val = generate_json(rng, settings, current_depth + 1);
                out.insert({ std::move(key), std::move(val) });
            }
            return out;
        }
        case kind::null:
        default:
            return null;
    }
}

class stopwatch
{
public:
    using clock      = std::chrono::steady_clock;
    using time_point = clock::time_point;
    using duration   = std::chrono::nanoseconds;

    struct ticker
    {
        stopwatch* owner;
        time_point start_time;

        ticker(stopwatch* o) :
                owner(o),
                start_time(clock::now())
        { }

        ticker(ticker&& src) :
                owner(src.owner),
                start_time(src.start_time)
        {
            src.owner = nullptr;
        }

        ~ticker()
        {
            if (owner)
                owner->add_time(clock::now() - start_time);
        }
    };

public:
    stopwatch() :
            tick_count(0),
            total_time(duration(0))
    { }

    ticker start()
    {
        return ticker(this);
    }

    void add_time(duration dur)
    {
        ++tick_count;
        total_time += dur;
    }

public:
    std::size_t tick_count;
    duration    total_time;
};

static std::string get_encoded_json()
{
    std::ifstream in("temp.json");
    if (in.good())
    {
        std::stringstream buff;
        buff << in.rdbuf();
        return buff.str();
    }

    std::ostringstream encoded_stream;
    ostream_pretty_encoder out(encoded_stream);
    std::mt19937_64 rng{std::random_device()()};
    generated_json_settings settings;
    value val = generate_json(rng, settings);
    out.encode(val);
    std::string encoded = encoded_stream.str();

    // save the string to a file in case we want to re-use it
    std::ofstream file("temp.json", std::ofstream::out | std::ofstream::trunc);
    file << encoded;
    return encoded;
}

int main(int argc, char** argv)
{
    using namespace json_benchmark;

    std::string filter;
    if (argc >= 2)
        filter = argv[1];

    int loop_count = 10;
    if (argc >= 3)
        loop_count = boost::lexical_cast<int>(argv[2]);

    std::string encoded = get_encoded_json();

    for (const benchmark_suite* suite : benchmark_suite::all())
    {
        if (!filter.empty() && filter != suite->name())
            continue;

        std::cout << suite->name() << "..." << std::endl;
        stopwatch watch;
        for (int idx = 1; idx <= loop_count; ++idx)
        {
            std::cout << "  " << idx << '/' << loop_count << std::endl;
            auto ticker = watch.start();
            suite->parse_test(encoded);
        }

        auto average = std::chrono::duration_cast<std::chrono::microseconds>(watch.total_time) / watch.tick_count;
        std::cout << suite->name() << '\t' << average.count() << "us" << std::endl;
    }
}