//  Copyright (c) 2014 Hartmut Kaiser
//  Copyright (c) 2014 Patricia Grubel
//  Copyright (c) 2020 Pranav Gadikar
//
//  SPDX-License-Identifier: BSL-1.0
//  Distributed under the Boost Software License, Version 1.0. (See accompanying
//  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

// Note on terminology  :-
// grid square  : small squares/ rectangles into which we have divided the
// larger mesh mesh points  : points within a single grid square which is
// smallest unit of space in our equation

#include <hpx/hpx_init.hpp>
#include <hpx/hpx.hpp>
#include <hpx/serialization/serialize.hpp>
#include <hpx/type_support/unused.hpp>
#include <hpx/include/performance_counters.hpp>

#include <boost/shared_array.hpp>

#include <cstddef>
#include <cstdint>
#include <cmath>
#include <iostream>
#include <vector>
#include <math.h>
#include <sys/time.h>
#include <unordered_map>
#include <valarray>
#include <iterator>
#include <algorithm>
#include <fstream>
#include <string.h>
#include <queue>
#include <utility>
#include <set>

#include "../include/point.h"
#include "../include/print_time_results.hpp"
#include "../include/writer.h"
#include "../include/Config.h"

#define ff first
#define ss second

bool header = 1;    //!< print csv heading
double k = 1;       //!< heat transfer coefficient
double dt = 1.;     //!< time step
double dh = 0.005;  //!< grid spacing
long nx = 20;       //!< Number of grid points in x dimension
long ny = 20;       //!< Number of grid points in y dimension
long npx =
    10;  //!< Number of partitions we want to break the grid in x-dimension
long npy =
    10;       //!< Number of partitions we want to break the grid in y-dimension
long nl = 1;  //!< Number of localities available for distributing the data
long eps = 5;       //!< Epsilon for influence zone of a point
double c_2d = 0.0;  //!< 'c' to be used in nonlocal equation for 2d case when
                    //!< J(influence function) = 1
std::vector<int> partition_domain;  //!< std::vector to store locality that
                                    //!< holds each partition
std::vector<hpx::performance_counters::performance_counter>
    perf_counters;  //!< std::vector of performance counters

//! operator for getting the location of 2d point in 1d representation
/**
 * @param x x coordinate in 2d cartesian coordinates
 * @param y y coordinate in 2d cartesian coordinates
 * @param nx length of the 2d cartesian plane in x dimension
 * @return location of 2d point in 1d array used to store the 2d mesh
 */
inline long get_loc(long x, long y, long nx = nx) { return x + y * nx; }

//! operator for getting 2d coordinate of grid square in which the space has
//! been divided
/**
 * Internally calls get_loc() function
 * @see get_loc()
 * @param x x coordinate in 2d cartesian coordinates
 * @param y y coordinate in 2d cartesian coordinates
 * @return location of 2d grid's square in 1d array used to store the 2d grid
 */
inline long grid_loc(long x, long y) { return get_loc(x / nx, y / ny, npx); }

//! operator for getting 2d coordinate of a point within the grid square
/**
 * Internally calls get_loc() function
 * @see get_loc()
 * @param x x coordinate in 2d cartesian coordinates
 * @param y y coordinate in 2d cartesian coordinates
 * @return location of 2d point within a grid square in 1d array used to store
 * the square subdomain
 */
inline long mesh_loc(long x, long y) { return get_loc(x % nx, y % ny); }

//! function to get the locality id of a particular partition
/**
 * @see partition_domain
 * @param i location of 2d point in 1d array used to store individual square
 * subdomains
 * @param nl number of localities avaible for distributing the computation
 * @return index of locality in the localities array
 */
inline std::size_t locidx(std::size_t i, std::size_t nl) {
  if (partition_domain.size() == 0)
    return (i * nl) / (npx * npy);
  else
    return partition_domain[i];
}

void setup_counters() {
  // We need to explicitly start all counters before we can use them. For
  // certain counters this could be a no-op, in which case start will return
  // 'false'.
  int num_loc = hpx::get_num_localities().get();

  // cannot test when the number of nodes is 1
  // because idle-rate performance counter is not available
  if (num_loc == 1) return;
  perf_counters.resize(num_loc);

  for (int idx = 0; idx < num_loc; ++idx) {
    perf_counters[idx] = hpx::performance_counters::performance_counter(
        "/threads{locality#" + std::to_string(idx) + "/total}/idle-rate");
    perf_counters[idx].start(hpx::launch::sync);
  }
}

/**
 * A small square partition of the original space in which we have divided the
 * large mesh into
 */
class partition_space {
 private:
  typedef hpx::serialization::serialize_buffer<double> buffer_type;
  //!< Data type of the serialized buffer

 public:
  long gx;  //!< x coordinate of the grid point corresponding the npx * npy
            //!< squares
  long gy;  //!< y coordinate of the grid point corresponding the npx * npy
            //!< squares

  /**
   * Empty constructor for initializing the partition_space class
   */
  explicit partition_space()
      : data_(std::allocator<double>().allocate(0), 0, buffer_type::take),
        size_(0),
        gx(0),
        gy(0) {}

  /**
   * Parameterized constructor for initializing the partition_space object
   * @param size size of the data buffer to be created in partition_space
   * @param gx x coordinate of the grid point corresponding the npx * npy
   * squares
   * @param gy y coordinate of the grid point corresponding the npx * npy
   * squares
   */
  partition_space(std::size_t size, long gx, long gy)
      : data_(std::allocator<double>().allocate(size), size, buffer_type::take),
        size_(size),
        gx(gx),
        gy(gy) {}

  //! function to add test initialization for executing tests
  /**
   * Parameterized constructor for initializing the partition_space object
   * @param nx size of square domain in x dimension
   * @param ny size of square domain in y dimension
   * @param gx x coordinate of the grid point corresponding the npx * npy
   * squares
   * @param gy y coordinate of the grid point corresponding the npx * npy
   * squares
   */
  partition_space(long nx, long ny, long gx, long gy)
      : data_(std::allocator<double>().allocate(nx * ny), nx * ny,
              buffer_type::take),
        size_(nx * ny),
        gx(gx),
        gy(gy) {
    for (long sx = gx * nx; sx < nx + gx * nx; ++sx) {
      for (long sy = gy * ny; sy < ny + gy * ny; ++sy) {
        data_[mesh_loc(sx, sy)] =
            sin(2 * M_PI * (sx * dh)) * sin(2 * M_PI * (sy * dh));
      }
    }
  }

  //! operator to return the data at some index from the given partition
  double& operator[](std::size_t idx) { return data_[idx]; }
  //! operator to return the data at some index from the given partition
  double operator[](std::size_t idx) const { return data_[idx]; }

  //! operator to return the size of the data partition in partition_space
  //! object
  std::size_t size() const { return size_; }

 private:
  friend class hpx::serialization::access;

  //! serialization support for transfering objects across localities
  template <typename Archive>
  void serialize(Archive& ar, const unsigned int version) {
    ar& data_& size_& gx& gy;
  }

  buffer_type
      data_;  //!< buffer to store the temperature values for a square subdomain
  std::size_t size_;  //!< size of the buffer storing the square subdomain
};

/**
 * server side of the data to access the component using it's global address
 */
struct partition_space_server
    : hpx::components::component_base<partition_space_server> {
  partition_space_server() {}

  /**
   * Parameterized constructor to create a partition_space_server object
   * for given temperature values in a square subdomain
   * @param data partition_space object storing temperature values in a square
   * subdomain
   */
  explicit partition_space_server(partition_space const& data) : data_(data) {}

  /**
   * Parameterized constructor for initializing the partition_space_server
   * object
   * @param size size of the data buffer to be created in partition_space
   * @param gx x coordinate of the grid point corresponding the npx * npy
   * squares
   * @param gy y coordinate of the grid point corresponding the npx * npy
   * squares
   */
  partition_space_server(std::size_t size, long gx, long gy)
      : data_(size, gx, gy) {}

  /**
   * Parameterized constructor for initializing the partition_space_server
   * object
   * @param nx size of square domain in x dimension
   * @param ny size of square domain in y dimension
   * @param gx x coordinate of the grid point corresponding the npx * npy
   * squares
   * @param gy y coordinate of the grid point corresponding the npx * npy
   * squares
   */
  partition_space_server(long nx, long ny, long gx, long gy)
      : data_(nx, ny, gx, gy) {}

  //! Access data. The parameter specifies what part of the data should be
  //! accessed. As long as the result is used locally, no data is copied,
  //! however as soon as the result is requested from another locality only
  //! then the data will be copied
  partition_space get_data() const { return data_; }

  //! Every member function which has to be invoked remotely needs to be
  //! wrapped into a component action. The macro below defines a new type
  //! 'get_data_action' which represents the (possibly remote) member function
  //! partition::get_data().
  HPX_DEFINE_COMPONENT_DIRECT_ACTION(partition_space_server, get_data,
                                     get_data_action);

 private:
  partition_space data_;  //!< partition_space object storing temperature values
                          //!< which is exposed by the server
};

//! HPX_REGISTER_COMPONENT() exposes the component creation
//! through hpx::new_<>().
typedef hpx::components::component<partition_space_server>
    partition_space_server_type;
HPX_REGISTER_COMPONENT(partition_space_server_type, partition_space_server);

//! HPX_REGISTER_ACTION() exposes the component member function for remote
//! invocation.
typedef partition_space_server::get_data_action get_data_action;
HPX_REGISTER_ACTION(get_data_action);

/**
 * client side code for accessing the partition server from possibly a different
 * locality
 */
struct partition_space_client
    : hpx::components::client_base<partition_space_client,
                                   partition_space_server> {
  typedef hpx::components::client_base<partition_space_client,
                                       partition_space_server>
      base_type;  //!< client base type for the partition_space object

  partition_space_client() {}

  //! Create new component on locality 'where' and initialize the held data
  /**
   * Parameterized constructor for initializing the partition_space_server
   * object
   * @param where locality where the held data is initialized
   * @param size size of the data buffer to be created in partition_space
   * @param gx x coordinate of the grid point corresponding the npx * npy
   * squares
   * @param gy y coordinate of the grid point corresponding the npx * npy
   * squares
   */
  partition_space_client(hpx::id_type where, std::size_t size, long gx, long gy)
      : base_type(hpx::new_<partition_space_server>(where, size, gx, gy)) {}

  //! Create new component on locality 'where' and initialize the held data
  /**
   * Parameterized constructor for initializing the partition_space_server
   * object
   * @param where locality where the held data is initialized
   * @param nx size of square domain in x dimension
   * @param ny size of square domain in y dimension
   * @param gx x coordinate of the grid point corresponding the npx * npy
   * squares
   * @param gy y coordinate of the grid point corresponding the npx * npy
   * squares
   */
  partition_space_client(hpx::id_type where, long nx, long ny, long gx, long gy)
      : base_type(hpx::new_<partition_space_server>(where, nx, ny, gx, gy)) {}

  /**
   * Create a new component on the locality co-located to the id 'where'. The
   * new instance will be initialized from the given partition_data
   * @param where locality where the held data is initialized
   * @param data partition_space object storing temperature values in a square
   * subdomain
   */
  partition_space_client(hpx::id_type where, partition_space const& data)
      : base_type(
            hpx::new_<partition_space_server>(hpx::colocated(where), data)) {}

  //! Attach a future representing a (possibly remote) partition.
  explicit partition_space_client(hpx::future<hpx::id_type>&& id)
      : base_type(std::move(id)) {}

  //! Unwrap a future<partition> (a partition already holds a future to the
  //! id of the referenced object, thus unwrapping accesses this inner future).
  partition_space_client(hpx::future<partition_space_client>&& c)
      : base_type(std::move(c)) {}

  //! Invoke the (remote) member function which gives us access to the data.
  //! This is a pure helper function hiding the async.
  hpx::future<partition_space> get_data() const {
    partition_space_server::get_data_action act;
    return hpx::async(act, get_id());
  }
};

/**
 * class which contains the code to solve the 2d nonlocal equation
 * in a distributed setup. The class contains functions to run the
 * solver in a distributed setup with support for logging at regular
 * intervals.
 */
class solver {
 public:
  //! 3d coordinate for representation of point
  typedef util::Point3 point_3d;

  //! 3d plane using the 3d point representation
  //! Note that the plane is actually stored in a 1d fashion
  typedef std::vector<point_3d> plane_3d;

  //! partitioning the 2d space into npx * npy small squares which constitute a
  //! partition of space
  typedef hpx::shared_future<partition_space> partition_fut;

  //! 2d space data structure which consists of future of square partitions
  typedef std::vector<partition_fut> space_2d_fut;

  //! 2d space representation for temperature data
  typedef std::vector<partition_space_client> space_2d;

  space_2d S[2];  //!< alternate for space between time steps,
  //!< result of S[t%2] goes to S[(t+1)%2] at every timestep 't'

  plane_3d
      P;  //!< vector containining 3d coordinates of the points in the plane

  long nt;        //!< number of timesteps
  long nlog;      //!< Number of time steps to log the results
  long nbalance;  //!< Number of time steps to run load balancing algorithm
  bool current;   //!< boolean variable to store index of current mesh which is
                  //!< already computed
  bool next;  //!< boolean variable to store index of next mesh which is being
              //!< computed
  bool test;  //!< true if the analyitcal solution needs to be compared with the
              //!< numerical solution

  double error_l2;    //!< l2 norm
  double error_linf;  //!< l infinity norm

  std::vector<hpx::id_type>
      localities;  //!< std::vector of id's of various available localitites for
                   //!< computation

  //! constructor to allocate and initialize class variables
  /**
   * Internally calls get_loc() function
   * @see partition_space_client
   * @see partition_space_server
   * @see partition_space
   * @param nt number of timesteps to execute the sover
   * @param nlog number of iterations after which we want to log
   * @param nbalance number of time steps to run load balancing algorithm
   * @param test true if numerical solution needs to be tested against
   * analytical solution
   * @param filename name of file to input the mesh from
   */
  solver(long nt, long nlog, long nbalance, bool test,
         const std::string& filename = "None") {
    this->nt = nt;
    this->nlog = nlog;
    this->nbalance = nbalance;
    this->current = 0;
    this->error_l2 = 0.0;
    this->error_linf = 0.0;
    this->test = test;
    this->next = 1;

    // take parameters input from a file
    if (filename != "None") param_file_input(filename);

    // forming the vector of localities id's and assigning the 'nl'
    std::vector<hpx::id_type> all_localities = hpx::find_all_localities();

    this->localities.resize(all_localities.size());

    // checking if the number of partitions is less than the number of
    // localities
    for (long idx = 0; idx < std::min((long)all_localities.size(), npx * npy);
         ++idx) {
      this->localities[hpx::naming::get_locality_id_from_id(
          all_localities[idx])] = all_localities[idx];
    }
    nl = this->localities.size();

    // setting the global variable c_2d which is a constant for this test case
    // as per the inputs
    c_2d = (k * 8) / pow(eps * dh, 4);

    P.resize(nx * ny * npx * npy);
    for (long sx = 0; sx < nx * npx; ++sx) {
      for (long sy = 0; sy < ny * npy; ++sy) {
        P[sx + sy * (nx * npx)] = point_3d(sx, sy, 0);
      }
    }
    nl = this->localities.size();

    for (space_2d& s : S) s.resize(npx * npy);

    // initial conditions for testing the correctness
    for (long i = 0; i < npx * npy; ++i)
      S[0][i] = partition_space_client(this->localities[locidx(i, nl)], nx, ny,
                                       i % npx, i / npx);
  }

  /**
   * Internally calls get_loc() function
   * @param filename name of file to input the mesh from
   */
  void param_file_input(const std::string& filename) {
    char char_array[filename.length() + 1];
    strcpy(char_array, filename.c_str());

    std::ifstream infile;
    infile.open(char_array);

    if (infile) {
      std::size_t px, py, loc;
      infile >> nx >> ny >> npx >> npy >> dh;
      partition_domain.resize(npx * npy);

      for (long idx = 0; idx < npx; ++idx) {
        for (long idy = 0; idy < npy; ++idy) {
          infile >> px >> py >> loc;
          partition_domain[get_loc(px, py, npx)] = loc;
        }
      }
    }

    infile.close();
  }

  /**
   * function to compute l2 norm of the solution
   * @param time time at which we want to check the analytical solution against
   * numerical solution
   */
  void compute_l2(long time) {
    auto solution = hpx::util::unwrap(vector_get_data(S[next]));
    error_l2 = 0;

    for (long sx = 0; sx < nx * npx; ++sx)
      for (long sy = 0; sy < ny * npy; ++sy)
        error_l2 += std::pow(
            solution[grid_loc(sx, sy)][mesh_loc(sx, sy)] - w(sx, sy, time), 2);
  }

  /**
   * function to compute l infinity norm of the solution
   * @param time time at which we want to check the analytical solution against
   * numerical solution
   */
  void compute_linf(long time) {
    auto solution = hpx::util::unwrap(vector_get_data(S[next]));
    error_linf = 0;

    for (long sx = 0; sx < nx * npx; ++sx)
      for (long sy = 0; sy < ny * npy; ++sy)
        error_linf =
            std::max(std::abs(solution[grid_loc(sx, sy)][mesh_loc(sx, sy)] -
                              w(sx, sy, time)),
                     error_linf);
  }

  /**
   * print error for testing
   * @param cmp true if we want the print the analytical solution comparison
   * against the numerical solution
   */
  void print_error(bool cmp) {
    compute_l2(nt);
    compute_linf(nt);

    std::cout << "l2: " << error_l2 << " linfinity: " << error_linf
              << std::endl;

    if (cmp) {
      auto solution = hpx::util::unwrap(vector_get_data(S[next]));
      for (long sx = 0; sx < nx * npx; ++sx) {
        for (long sy = 0; sy < ny * npy; ++sy) {
          std::cout << "sx: " << sx << " sy: " << sy
                    << " Expected: " << w(sx, sy, nt) << " Actual: "
                    << solution[grid_loc(sx, sy)][mesh_loc(sx, sy)]
                    << std::endl;
        }
        std::cout << std::endl;
      }
    }
  }

  //! print the solution for the user
  void print_soln() {
    auto solution = hpx::util::unwrap(vector_get_data(S[next]));

    for (long sx = 0; sx < nx * npx; ++sx) {
      for (long sy = 0; sy < ny * npy; ++sy) {
        std::cout << "S[" << sx << "][" << sy
                  << "] = " << solution[grid_loc(sx, sy)][mesh_loc(sx, sy)]
                  << " ";
      }
      std::cout << std::endl;
    }
  }

  /**
   * function to perform the logging in both csv and vtk format
   * @param temp_data temperature data for 2d square subdomain
   * @param coord 3d coordinates for points in Point3 format
   * @param time time for which we are logging the results
   * @param test variable is true if we have to test the numerical solution
   * against the analytical solution
   */
  static void log_csv_vtk(std::vector<partition_space> temp_data,
                          plane_3d coord, long time, bool test) {
    // file to store the simulation results in csv format
    const std::string simulate_fname = "../out_csv/simulate_2d.csv";

    // file to store l2 and l infinity norms per timestep
    const std::string score_fname = "../out_csv/score_2d.csv";

    std::ofstream outfile;
    outfile.open(simulate_fname, std::ios_base::app);

    for (long sx = 0; sx < nx * npx; ++sx) {
      for (long sy = 0; sy < ny * npy; ++sy) {
        outfile << time << "," << sx << "," << sy << ","
                << temp_data[grid_loc(sx, sy)][mesh_loc(sx, sy)] << ","
                << w(sx, sy, time) << ","
                << std::pow((temp_data[grid_loc(sx, sy)][mesh_loc(sx, sy)] -
                             w(sx, sy, time)),
                            2)
                << ","
                << std::abs(temp_data[grid_loc(sx, sy)][mesh_loc(sx, sy)] -
                            w(sx, sy, time))
                << ",\n";
      }
    }

    outfile.close();

    // VTK writer begins here

    // vtu file name for writing the vtk data
    const std::string fname = "../out_vtk/simulate_" + std::to_string(time);
    rw::writer::VtkWriter vtk_logger(fname);
    std::vector<double> vector_data(nx * ny * npx * npy);

    vtk_logger.appendNodes(&coord);

    for (long sx = 0; sx < nx * npx; ++sx)
      for (long sy = 0; sy < ny * npy; ++sy)
        vector_data[sy * nx * npx + sx] =
            temp_data[grid_loc(sx, sy)][mesh_loc(sx, sy)];

    vtk_logger.appendPointData("Temperature", &vector_data);
    vtk_logger.addTimeStep(std::time(0));
    vtk_logger.close();

    // VTK writer ends here

    // add to the score csv only when it's required
    if (test) {
      double error_l2 = 0, error_linf = 0;

      for (long sx = 0; sx < nx * npx; ++sx) {
        for (long sy = 0; sy < ny * npy; ++sy) {
          error_linf =
              std::max(std::abs(temp_data[grid_loc(sx, sy)][mesh_loc(sx, sy)] -
                                w(sx, sy, time)),
                       error_linf);
          error_l2 += std::pow(
              temp_data[grid_loc(sx, sy)][mesh_loc(sx, sy)] - w(sx, sy, time),
              2);
        }
      }

      std::ofstream outfile;

      outfile.open(score_fname, std::ios_base::app);
      outfile << time << "," << error_l2 << "," << error_linf << ",\n";
      outfile.close();
    }
  }

  /**
   * @see load_balance
   * function for checking the load balancing and visualizing the same
   * using hpx::performance counters
   */
  void test_load_balance() {
    double busy_rates[nl];  // idle rate values returned by performance counters
    double expected_busy_rate =
        0.0;  // hypotheticallly expected idle rate for equal distribution of
              // work among all nodes
    double max_diff = 0.0;  // variable to store the maximum difference between
                            // expected and actual idle rate
    std::cout << "Testing load balance:" << std::endl;

    // retrieve the performance counter values for idle-rates
    for (int idx = 0; idx < nl; ++idx) {
      hpx::performance_counters::counter_value value1 =
          perf_counters[idx].get_counter_value(hpx::launch::sync, true);
      busy_rates[idx] = 10000 - value1.get_value<double>();
      std::cout << "Test: counter value: " << busy_rates[idx] << std::endl;

      expected_busy_rate += busy_rates[idx];
    }
    expected_busy_rate /= nl;
    std::cout << "Expected busy rate " << expected_busy_rate << std::endl;

    for (int idx = 0; idx < nl; ++idx) {
      max_diff =
          std::max(std::abs(expected_busy_rate - busy_rates[idx]), max_diff);
    }

    std::cout << "Visualizing Load Balance across nodes" << std::endl;

    for (long idx = 0; idx < npx; ++idx) {
      for (long idy = 0; idy < npy; ++idy) {
        std::cout << partition_domain[get_loc(idx, idy, npx)] << " ";
      }
      std::cout << std::endl;
    }

    if (max_diff > 1500)
      std::cout << "Load not balanced correctly" << std::endl;
    else
      std::cout << "Load balanced correctly" << std::endl;
  }

  /**
   * function for bfs for extending the domain corresponding to each of the
   * nodes uniformly in all possible directions so that the change of shape in
   * the region corresponding to a node does not change much
   * @param node_id node id of the region whose boundary is being extended or
   * contracted
   * @param node_subdomain a square subdomain's id in the region belonging to
   * the current node
   * @param locality_ids locality id to which a particular square subdomain is
   * attached
   * @param visited_subdomain data structure to keep a track of sudomains
   * corresponding to current node
   * @param visited_node data structure to keep a track of visited nodes
   * @param work_realloc number of subdomains that need to be exchanged by
   * various nodes
   * @param total_subdomains counter for each nodes denoting number of square
   * subdomains for each of the nodes
   */
  void locality_subdomain_bfs(int node_id, int node_subdomain,
                              std::vector<std::vector<int> >& locality_ids,
                              int visited_subdomain[], int visited_node[],
                              int work_realloc[], int total_subdomains[]) {
    long ctr_2 = 1;
    long time = 0;

    // prioirity queue instead of normal queue so that we can reach boundary
    // points faster and expand all the boundaries evenly
    std::priority_queue<std::pair<bool, std::pair<long, long> > > bfs_q;
    std::pair<bool, std::pair<long, long> > q_element;
    bfs_q.push({1, {0, node_subdomain}});
    // variable to indicate whether we are currently expanding or contracting
    // the horizon
    bool invard_bfs = 0;
    time++;

    while (!bfs_q.empty() && work_realloc[node_id] != 0) {
      long ctr_1 = 0;
      for (long id = 0; id < ctr_2; ++id) {
        q_element = bfs_q.top();
        bfs_q.pop();
        long gx = q_element.ss.ss % npx, gy = q_element.ss.ss / npx;
        // we have reached the boundary and want to expand inward
        if (q_element.ff == 0 && work_realloc[node_id] < 0) invard_bfs = 1;
        visited_subdomain[q_element.ss.ss] = 1 + invard_bfs;

        for (long idx = std::max((long)0, gx - 1); idx < std::min(npx, gx + 2);
             ++idx) {
          for (long idy = std::max((long)0, gy - 1);
               idy < std::min(npy, gy + 2); ++idy) {
            if (std::abs(idx - gx) + std::abs(idy - gy) > 1 ||
                visited_subdomain[get_loc(idx, idy, npx)] == 1 + invard_bfs ||
                visited_node[locality_ids[idx][idy]] == 2)
              continue;

            bool same_locality = (locality_ids[idx][idy] == node_id);

            if (work_realloc[node_id] > 0) {
              bfs_q.push({same_locality, {time, get_loc(idx, idy, npx)}});
              ctr_1++;
            } else if (work_realloc[node_id] < 0 && invard_bfs == 0) {
              bfs_q.push({same_locality, {time, get_loc(idx, idy, npx)}});
              ctr_1++;
            } else if (work_realloc[node_id] < 0 && invard_bfs == 1 &&
                       same_locality && total_subdomains[node_id] > 1) {
              bfs_q.push({0, {time, get_loc(idx, idy, npx)}});
              ctr_1++;

              work_realloc[locality_ids[gx][gy]]--;
              work_realloc[node_id]++;

              total_subdomains[node_id]--;
              total_subdomains[locality_ids[gx][gy]]--;

              locality_ids[idx][idy] = locality_ids[gx][gy];
            }
          }
        }

        // we have reached the boundary and want to expand outward
        // at the same time, we want to expand only to those subdomains
        // whose localities have not been visited yet
        if (visited_node[locality_ids[gx][gy]] != 2 &&
            locality_ids[gx][gy] != node_id && work_realloc[node_id] > 0 &&
            total_subdomains[locality_ids[gx][gy]] > 1) {
          work_realloc[locality_ids[gx][gy]]++;
          work_realloc[node_id]--;

          total_subdomains[node_id]++;
          total_subdomains[locality_ids[gx][gy]]--;

          locality_ids[gx][gy] = node_id;
        }

        time++;
      }
      ctr_2 = ctr_1;
    }
  }

  /**
   * function for inserting the rectangles which are just above, below and on
   * the sides of the given partition into the unordered_map used to index the
   * squares already inserted into the vector
   * @see locality_subdomain_bfs
   * @param parent_id node id of the parent in the DFS
   * @param node_id node id of the region whose boundary is being extended or
   * contracted
   * @param locality_ids locality id to which a particular square subdomain is
   * attached
   * @param adj_set adjacency list for each of the nodes
   * @param visited_subdomain data structure to keep a track of sudomains
   * corresponding to current node
   * @param visited_node data structure to keep a track of visited nodes
   * @param work_realloc number of subdomains that need to be exchanged by
   * various nodes
   * @param node_subdomain std::vector containing a square subdomain's id in the
   * region belonging to the various nodes
   * @param total_subdomains counter for each nodes denoting number of square
   * subdomains for each of the nodes
   */
  void redistribution_dfs(int parent_id, int node_id,
                          std::vector<std::vector<int> >& locality_ids,
                          std::set<int> adj_set[], int visited_subdomain[],
                          int visited_node[], int work_realloc[],
                          int node_subdomain[], int total_subdomains[]) {
    visited_node[node_id] = 1;
    for (auto itr = adj_set[node_id].begin(); itr != adj_set[node_id].end();
         ++itr) {
      if (visited_node[*itr] == 0) {
        redistribution_dfs(node_id, *itr, locality_ids, adj_set,
                           visited_subdomain, visited_node, work_realloc,
                           node_subdomain, total_subdomains);
      }
    }

    // The first partition doesn't have any partition left to exchange with it
    if (parent_id == -1) return;
    // perform subdomain exchanges as required with other partitions
    locality_subdomain_bfs(node_id, node_subdomain[node_id], locality_ids,
                           visited_subdomain, visited_node, work_realloc,
                           total_subdomains);

    visited_node[node_id] = 2;
  }

  /**
   * Function to run the load balancing algorithm every nbalance time steps.
   * Function reassigns localities to various square subdomains(if any) to
   * have balanced computation across all the nodes in the distributed setup.
   * @see locality_subdomain_bfs
   * @see redistribution_dfs
   * @param unbalanced_data std::vector of client-side data structures for
   * storing temperature data
   * @return std::vector of client-side data structures with some square
   * subdomains possibly reassigned to different nodes
   */
  std::vector<partition_space_client> load_balance(
      std::vector<partition_space_client> unbalanced_data) {
    // hpx::naming::get_locality_id_from_id
    double busy_rates[nl];  // idle rate values returned by performance counters
    int work_realloc[nl];   // amount of subdomains a particular node is capable
                            // of computing more
    int total_subdomains[nl];  // total number of subdomains present on the node
    double expected_busy_rate =
        0.0;  // hypotheticallly expected idle rate for equal distribution of
              // work among all nodes

    // retrieve the performance counter values for idle-rates
    for (int idx = 0; idx < nl; ++idx) {
      hpx::performance_counters::counter_value value1 =
          perf_counters[idx].get_counter_value(hpx::launch::sync, true);
      busy_rates[idx] = 10000 - value1.get_value<double>();

      expected_busy_rate += busy_rates[idx];
      total_subdomains[idx] = 0;
    }
    expected_busy_rate /= nl;

    std::set<int> adj_set[nl];  // adjacency list of the domain partitions graph
                                // as a set to prevent repetitions
    int visited_node[nl];  // true if a particular node has already been visited
                           // during DFS
    int node_subdomain[nl];  // contains subdomain id for each of the nodes to
                             // start the BFS on each node
    int visited_subdomain[npx *
                          npy];  // true if a particular subdomain has already
                                 // been visited during load balancing
    std::vector<std::vector<int> >
        locality_ids;  // locality id to which a particular element is attached
    locality_ids.resize(npx);
    for (long idx = 0; idx < npx; ++idx) locality_ids[idx].resize(npy);

    for (long gx = 0; gx < npx; ++gx) {
      for (long gy = 0; gy < npy; ++gy) {
        std::size_t locality_id = locidx(get_loc(gx, gy, npx), nl);
        bool is_boundary = 0;
        for (long idx = std::max((long)0, gx - 1); idx < std::min(npx, gx + 2);
             ++idx) {
          for (long idy = std::max((long)0, gy - 1);
               idy < std::min(npy, gy + 2); ++idy) {
            if (std::abs(idx - gx) + std::abs(idy - gy) > 1) continue;
            if (locidx(get_loc(idx, idy, npx), nl) != locality_id) {
              adj_set[locality_id].insert(locidx(get_loc(idx, idy, npx), nl));
            }
          }
        }

        node_subdomain[locality_id] = get_loc(gx, gy, npx);
        total_subdomains[locality_id]++;
        visited_subdomain[get_loc(gx, gy, npx)] = 0;
        locality_ids[gx][gy] = locality_id;
      }
    }

    int min_load_imbalance = INT32_MAX;
    int node_min_imbalance = 0;

    // calculate the excess and the deficiency of compute across all the nodes
    for (int idx = 0; idx < nl; ++idx) {
      double time_per_subdomain =
          busy_rates[idx] / (double)total_subdomains[idx];
      if (expected_busy_rate > busy_rates[idx] &&
          abs(expected_busy_rate - busy_rates[idx]) > 0.3 * time_per_subdomain)
        work_realloc[idx] =
            ceil((expected_busy_rate - busy_rates[idx]) / time_per_subdomain);
      else if (expected_busy_rate < busy_rates[idx] &&
               abs(expected_busy_rate - busy_rates[idx]) >
                   0.3 * time_per_subdomain)
        work_realloc[idx] =
            floor((expected_busy_rate - busy_rates[idx]) / time_per_subdomain);
      else
        work_realloc[idx] = 0;

      if (abs(work_realloc[idx]) < min_load_imbalance) {
        min_load_imbalance = abs(work_realloc[idx]);
        node_min_imbalance = idx;
      }

      visited_node[idx] = 0;
    }

    // positive work_realloc means requires more work for optimal distribution
    // perform the redistribution of domains and ressign the locality ids to
    // balance the load set the root node as the "compute node" with minimum
    // load imbalance
    redistribution_dfs(-1, node_min_imbalance, locality_ids, adj_set,
                       visited_subdomain, visited_node, work_realloc,
                       node_subdomain, total_subdomains);

    // perform the reassignment of localities for each of the subdomains and
    // return them
    for (long idx = 0; idx < npx; ++idx)
      for (long idy = 0; idy < npy; ++idy)
        unbalanced_data[get_loc(idx, idy, npx)] = partition_space_client(
            this->localities[locality_ids[idx][idy]],
            hpx::util::unwrap(
                unbalanced_data[get_loc(idx, idy, npx)].get_data()));

    if (partition_domain.size() == 0) partition_domain.resize(npx * npy);

    for (long idx = 0; idx < npx; ++idx)
      for (long idy = 0; idy < npy; ++idy)
        partition_domain[get_loc(idx, idy, npx)] = locality_ids[idx][idy];

    // reset all counters for more accurate time and remove the data transfer
    // times from the idle-rate
    for (int idx = 0; idx < nl; ++idx)
      hpx::performance_counters::counter_value value1 =
          perf_counters[idx].get_counter_value(hpx::launch::sync, true);

    return unbalanced_data;
  }

  /**
   * our influence function to weigh various points differently
   * @param distance distance of the point from point of computation
   * @return returns the influence of the current point for point under
   * computation
   */
  static inline double influence_function(double distance) { return 1.0; }

  /**
   * function for inserting the rectangles which are just above, below and on
   * the sides of the given partition into the unordered_map used to index the
   * squares already inserted into the vector
   * @param lx rectangle's lower left point's x coordinate
   * @param ly rectangle's lower left point's y coordinate
   * @param rx rectangle's upper right point's x coordinate
   * @param ry rectangle's upper right point's y coordinate
   * @param all_squares std::vector containing npx * npy squares from the
   * partitioned grid
   * @param neighbour_squares std::vector containing the neighbour square
   * subdomains from the current square
   */
  static void add_neighbour_rectangle(long lx, long ly, long rx, long ry,
                                      space_2d& all_squares,
                                      space_2d& neighbour_squares) {
    for (long sx = lx; sx < rx + nx; sx += nx) {
      for (long sy = ly; sy < ry + ny; sy += ny) {
        if (sx < 0 || sx >= nx * npx || sy < 0 || sy >= ny * npy) continue;

        neighbour_squares.push_back(all_squares[grid_loc(sx, sy)]);
      }
    }
  }

  /**
   * testing operator to verify correctness
   * @param pos_x x coordinate of the point of computation
   * @param pos_y y coordinate of the point of computation
   * @param time time at which we want the analytical solution
   * @return analytical solution's expected value at point (pos_x, pos_y)
   */
  static inline double w(long pos_x, long pos_y, long time) {
    return cos(2 * M_PI * (time * dt)) * sin(2 * M_PI * (pos_x * dh)) *
           sin(2 * M_PI * (pos_y * dh));
  }

  /**
   * condition to enforce the boundary conditions for the divided partitions
   * @param pos_x x coordinate of the point of computation
   * @param pos_y y coordinate of the point of computation
   * @param all_squares std::vector containing npx * npy squares from the
   * partitioned grid
   * @return 0 for points outside the mesh, else the value computed by the
   * numerical solution for point (pos_x, pos_y)
   */
  static inline double boundary(
      long pos_x, long pos_y, const std::vector<partition_space>& all_squares) {
    if (pos_x >= 0 && pos_x < nx * npx && pos_y >= 0 && pos_y < ny * npy)
      return all_squares[grid_loc(pos_x, pos_y)][mesh_loc(pos_x, pos_y)];
    else
      return 0;
  }

  /**
   * condition to enforce the boundary conditions for the tests of analytical
   * solution
   * @param pos_x x coordinate of the point of computation
   * @param pos_y y coordinate of the point of computation
   * @param val expected value for analytical solution at the point (pos_x,
   * pos_y)
   * @return 0 for points outside the mesh, else the value computed by the
   * analytical solution for point (pos_x, pos_y)
   */
  static inline double boundary(long pos_x, long pos_y, double val) {
    if (pos_x >= 0 && pos_x < nx * npx && pos_y >= 0 && pos_y < ny * npy)
      return val;
    else
      return 0;
  }

  /**
   * Function to find distance in 2d plane given 2 points coordinates
   * @param cen_x x coordinate of the point of computation
   * @param cen_y y coordinate of the point of computation
   * @param pos_x x coordinate of the point that influences the temperature at
   * (cen_x, cen_y)
   * @param pos_y y coordinate of the point that influences the temperature at
   * (cen_x, cen_y)
   * @return distance of point (cen_x, cen_y) from point (pos_x, pos_y)
   */
  static inline double distance(long cen_x, long cen_y, long pos_x,
                                long pos_y) {
    return sqrt(((cen_x - pos_x) * (cen_x - pos_x)) +
                ((cen_y - pos_y) * (cen_y - pos_y)));
  }

  //! Function to compute length of 3rd side of a right angled triangle
  //! given hypotenuse and lenght of one side
  static inline double len_1d_line(long len_x) {
    return sqrt((eps * eps) - (len_x * len_x));
  }

  /**
   * function adds the external source to the 2d nonlocal heat equation for
   * testing correctness of numerical solution against the analytical solution
   * @see sum_local
   * @param pos_x x coordinate of the point whose numerical solution is to be
   * computed
   * @param pos_y y coordinate of the point whose numerical solution is to be
   * computed
   * @param time time at which we want the numerical solution
   * @return temperature value at point (pos_x, pos_y) for analytical solution
   */
  static double sum_local_test(long pos_x, long pos_y, long time) {
    double result_local =
        -(2 * M_PI * sin(2 * M_PI * (time * dt)) *
          sin(2 * M_PI * (pos_x * dh)) * sin(2 * M_PI * (pos_y * dh)));
    double w_position = w(pos_x, pos_y, time);

    for (long sx = pos_x - eps; sx <= pos_x + eps; ++sx) {
      long len_line = len_1d_line(std::abs((long)pos_x - (long)sx));
      for (long sy = pos_y - len_line; sy <= pos_y + len_line; ++sy) {
        result_local -=
            influence_function(distance(pos_x, pos_y, sx, sy)) * c_2d *
            (boundary(sx, sy, w(sx, sy, time)) - w_position) * (dh * dh);
      }
    }

    return result_local;
  }

  /**
   * Our operator to find sum of 'eps' radius circle in vicinity of point P(x,y)
   * Represent circle as a series of horizaontal lines of thickness 'dh'
   * @param pos_x x coordinate of the point whose numerical solution is to be
   * computed
   * @param pos_y y coordinate of the point whose numerical solution is to be
   * computed
   * @param all_squares std::vector containing npx * npy squares from the
   * partitioned grid
   * @return temperature value at point (pos_x, pos_y) for numerical solution
   */
  static double sum_local(long pos_x, long pos_y,
                          const std::vector<partition_space>& all_squares) {
    double result_local = 0.0;
    double pos_val = boundary(pos_x, pos_y, all_squares);

    for (long sx = pos_x - eps; sx <= pos_x + eps; ++sx) {
      long len_line = len_1d_line(std::abs((long)pos_x - (long)sx));
      for (long sy = pos_y - len_line; sy <= pos_y + len_line; ++sy) {
        result_local += influence_function(distance(pos_x, pos_y, sx, sy)) *
                        c_2d * (boundary(sx, sy, all_squares) - pos_val) *
                        (dh * dh);
      }
    }

    return result_local;
  }

  //! Function to retrieve the data from possibly remote partitions for
  //! vector of square sundomains
  static space_2d_fut vector_get_data(
      const std::vector<partition_space_client>& neighbour_squares) {
    space_2d_fut ret_get_data;
    ret_get_data.resize(neighbour_squares.size());

    for (long idx = 0; idx < neighbour_squares.size(); ++idx) {
      ret_get_data[idx] = neighbour_squares[idx].get_data();
    }

    return ret_get_data;
  }

  /**
   * partition of nx * ny cells which will be processed by a single thread to
   * enable full utilization of all cores across all nodes in the cluster
   * @param middle x coordinate of the point whose numerical solution is to be
   * computed
   * @param neighbour_squares std::vector containing the neighbour square
   * subdomains from the current square
   * @param time time for which we are logging the results
   * @param test variable is true if we have to test the numerical solution
   * against the analytical solution
   * @return client side data structure for temperature values for a square
   * subdomain
   */
  static partition_space_client sum_local_partition(
      partition_space_client const& middle,
      const std::vector<partition_space_client>& neighbour_squares, long time,
      bool test) {
    // all_squares is a vector with partitions with pointers to valid partitions
    // to only those neighbours who affect the temperature at the middle
    // partition

    hpx::shared_future<partition_space> middle_data = middle.get_data();

    hpx::future<partition_space> next_middle =
        middle_data.then(hpx::util::unwrapping(
            [time, test](partition_space const& mid) -> partition_space {
              std::vector<partition_space> all_squares;
              all_squares.resize(npx * npy);
              // All local operations are performed once the middle data of
              // the previous time step becomes available.
              long gx = mid.gx, gy = mid.gy;
              partition_space next(mid.size(), gx, gy);
              all_squares[get_loc(gx, gy, npx)] = mid;

              for (long sx = eps + 1; sx < nx - eps - 1; ++sx) {
                for (long sy = eps + 1; sy < ny - eps - 1; ++sy) {
                  next[get_loc(sx, sy)] =
                      mid[get_loc(sx, sy)] +
                      (sum_local(sx + gx * nx, sy + gy * ny, all_squares) * dt);
                  if (test)
                    next[get_loc(sx, sy)] +=
                        sum_local_test(sx + gx * nx, sy + gy * ny, time) * dt;
                }
              }
              return next;
            }));

    return hpx::dataflow(
        hpx::launch::async,
        hpx::util::unwrapping([middle, time, test](
                                  partition_space next,
                                  partition_space const& mid,
                                  const std::vector<partition_space>&
                                      neighbour_squares)
                                  -> partition_space_client {
          // Calculate the missing boundary elements once the
          // corresponding data has become available.
          std::vector<partition_space> all_squares;
          all_squares.resize(npx * npy);
          long gx = mid.gx, gy = mid.gy;

          // assign only those partition pointers which are valid neighbours for
          // a particular square in the grid
          for (long idx = 0; idx < neighbour_squares.size(); ++idx) {
            all_squares[get_loc(neighbour_squares[idx].gx,
                                neighbour_squares[idx].gy, npx)] =
                neighbour_squares[idx];
          }

          if (nx <= eps) {
            for (long sx = 0; sx < nx; ++sx) {
              for (long sy = 0; sy < ny; ++sy) {
                next[get_loc(sx, sy)] =
                    mid[get_loc(sx, sy)] +
                    (sum_local(sx + gx * nx, sy + gy * ny, all_squares) * dt);
                if (test)
                  next[get_loc(sx, sy)] +=
                      sum_local_test(sx + gx * nx, sy + gy * ny, time) * dt;
              }
            }
          } else {
            for (long sx = 0; sx < eps + 1; ++sx) {
              for (long sy = 0; sy < ny; ++sy) {
                next[get_loc(sx, sy)] =
                    mid[get_loc(sx, sy)] +
                    (sum_local(sx + gx * nx, sy + gy * ny, all_squares) * dt);
                if (test)
                  next[get_loc(sx, sy)] +=
                      sum_local_test(sx + gx * nx, sy + gy * ny, time) * dt;
              }
            }

            for (long sx = eps + 1; sx < nx - eps - 1; ++sx) {
              for (long sy = 0; sy < eps + 1; ++sy) {
                next[get_loc(sx, sy)] =
                    mid[get_loc(sx, sy)] +
                    (sum_local(sx + gx * nx, sy + gy * ny, all_squares) * dt);
                if (test)
                  next[get_loc(sx, sy)] +=
                      sum_local_test(sx + gx * nx, sy + gy * ny, time) * dt;
              }
              for (long sy = ny - eps - 1; sy < ny; ++sy) {
                next[get_loc(sx, sy)] =
                    mid[get_loc(sx, sy)] +
                    (sum_local(sx + gx * nx, sy + gy * ny, all_squares) * dt);
                if (test)
                  next[get_loc(sx, sy)] +=
                      sum_local_test(sx + gx * nx, sy + gy * ny, time) * dt;
              }
            }

            for (long sx = nx - eps - 1; sx < nx; ++sx) {
              for (long sy = 0; sy < ny; ++sy) {
                next[get_loc(sx, sy)] =
                    mid[get_loc(sx, sy)] +
                    (sum_local(sx + gx * nx, sy + gy * ny, all_squares) * dt);
                if (test)
                  next[get_loc(sx, sy)] +=
                      sum_local_test(sx + gx * nx, sy + gy * ny, time) * dt;
              }
            }
          }

          // The new partition_data will be allocated on the same locality
          // as 'middle'.
          return partition_space_client(middle.get_id(), next);
        }),
        std::move(next_middle), middle_data,
        vector_get_data(neighbour_squares));
  }

  void do_work();
};

HPX_PLAIN_ACTION(solver::sum_local_partition, sum_local_partition_action);

//! Compute the numerical solution on 'nx * ny' data points for each of 'npx *
//! npy' partitions for 'nt' time steps
void solver::do_work() {
  // Actual time step loop
  for (long t = 0; t < nt; ++t) {
    current = t % 2;
    next = (t + 1) % 2;

    sum_local_partition_action act;
    for (long gx = 0; gx < npx; ++gx) {
      for (long gy = 0; gy < npy; ++gy) {
        // we execute the action on the locality of the middle partition
        auto Op = hpx::util::bind_front(
            act, localities[locidx(get_loc(gx, gy, npx), nl)]);

        // vector of dependent grid squares for a particular grid squares
        space_2d vector_deps;

        // add dependent neighbouring squares
        // for now they are added assuming it to be a big rectangle
        // in the future these additions can be in form of 4 sectors for 4
        // corners and 4 rectangles for top, bottom, left and right of the
        // rectangle
        add_neighbour_rectangle(gx * nx - eps, gy * ny - eps,
                                gx * nx + nx + eps, gy * ny + ny + eps,
                                S[current], vector_deps);

        // represent dependencies using hpx dataflow
        S[next][get_loc(gx, gy, npx)] = hpx::dataflow(
            hpx::launch::async, Op, S[current][get_loc(gx, gy, npx)],
            vector_deps, t, test);
      }
    }

    // launch a seperate thread for running the load balancing algorithm
    // and assign back the changed array of localities(if any) by the
    // load balancing algorithm
    if (t % nbalance == 0 && t != 0 && nl > 1) {
      for (int idx = 0; idx < npx * npy; ++idx) S[next][idx].get_data().wait();
      S[next] = load_balance(S[next]);
    }

    // launch a seperate thread for collecting the logging data from
    // various threads and output in the required files
    if (t % nlog == 0)
      hpx::dataflow(hpx::launch::async,
                    hpx::util::unwrapping(&solver::log_csv_vtk),
                    vector_get_data(S[next]), P, t, test);
  }

  // required for correctness for the case when nt = 0
  // because in that case next won't be set in the above for loop
  next = nt % 2;

  // wait for the solution to be ready for all the partitions
  for (int idx = 0; idx < npx * npy; ++idx) S[next][idx].get_data().wait();
}

//! function to execute the ctests which are there in file '2d_distributed.txt'
int batch_tester(long nlog, long nbalance) {
  std::uint64_t nt, num_tests;
  bool test_failed = 0;

  std::cin >> num_tests;
  for (long i = 0; i < num_tests; ++i) {
    std::cin >> nx >> ny >> npx >> npy >> nt >> eps >> k >> dt >> dh;

    // Create the solver object
    solver solve(nt, nlog, nbalance, 1);

    // do the actual error
    solve.do_work();

    // compute and print the error for checking the difference between
    // analytical and numerical solution
    solve.compute_l2(nt);

    if (solve.error_l2 / (double)(nx * ny * npx * npy) > 1e-6) {
      test_failed = 1;
      break;
    }
  }

  // output regular expression whether the test passed or failed
  if (test_failed)
    std::cout << "Tests Failed" << std::endl;
  else
    std::cout << "Tests Passed" << std::endl;

  return hpx::finalize();
}

//! main function in the HPX runtime
int hpx_main(hpx::program_options::variables_map& vm) {
  setup_counters();

  std::uint64_t nt = vm["nt"].as<std::uint64_t>();  // Number of steps.
  std::uint64_t nlog =
      vm["nlog"]
          .as<std::uint64_t>();  // Number of time steps to log the results
  std::uint64_t nbalance =
      vm["nbalance"].as<std::uint64_t>();  // Number of time steps to run the
                                           // load balancing algorithm
  std::string filename =
      vm["file"].as<std::string>();   // Filename to take input from
  bool test = vm["test"].as<bool>();  // Boolean variable to indicate if
                                      // numerical solution needs to be tested
  if (nx <= eps)
    std::cout << "[WARNING] Mesh size on a single node (nx * ny) is too small "
                 "for given epsilon (eps)"
              << std::endl;

  if (vm.count("no-header")) header = false;

  // batch testing for ctesting
  if (vm.count("test_batch")) return batch_tester(nlog, nbalance);

  // Create the solver object
  solver solve(nt, nlog, nbalance, test, filename);

  // Measure execution time.
  std::uint64_t t = hpx::util::high_resolution_clock::now();

  // Execute nt time steps on nx grid points.
  solve.do_work();

  std::uint64_t elapsed = hpx::util::high_resolution_clock::now() - t;

  if (vm.count("test_load_balance")) solve.test_load_balance();

  // print the calculated error
  // print comparison only when the 'cmp' flag is set
  if (test) solve.print_error(vm["cmp"].as<bool>());

  // Print the final solution
  if (vm.count("results")) solve.print_soln();

  std::uint64_t const num_worker_threads = hpx::get_num_worker_threads();
  hpx::future<std::uint32_t> locs = hpx::get_num_localities();
  print_time_results(locs.get(), num_worker_threads, elapsed, nx, ny, npx, npy,
                     nt, header);

  return hpx::finalize();
}

//! main function to create and initialize an HPX runtime
int main(int argc, char* argv[]) {
  std::cout << argv[0] << " (" << MAJOR_VERSION << "." << MINOR_VERSION << "."
            << UPDATE_VERSION << ")" << std::endl;
  namespace po = hpx::program_options;

  po::options_description desc_commandline;
  desc_commandline.add_options()(
      "test", po::value<bool>()->default_value(true),
      "use arguments from numerical solution for testing (default: false)")(
      "test_batch",
      "test the solution against numerical solution against batch inputs "
      "(default: false)")("test_load_balance",
                          "test the load balancing algorithm for approximately "
                          "equal load balancing "
                          "(default: false)")(
      "results", "print generated results (default: false)")(
      "cmp", po::value<bool>()->default_value(false),
      "Print expected versus actual outputs")(
      "file", po::value<std::string>()->default_value("None"),
      "name of file to take input from")(
      "nx", po::value<long>(&nx)->default_value(25), "Local x dimension")(
      "ny", po::value<long>(&ny)->default_value(25), "Local y dimension")(
      "nt", po::value<std::uint64_t>()->default_value(45),
      "Number of time steps")("npx", po::value<long>(&npx)->default_value(2),
                              "Number of partitions in x dimension")(
      "npy", po::value<long>(&npy)->default_value(2),
      "Number of partitions in y dimension")(
      "nlog", po::value<std::uint64_t>()->default_value(5),
      "Number of time steps to log the results")(
      "nbalance", po::value<std::uint64_t>()->default_value(INT64_MAX),
      "Number of time steps to run the load balancing algorithm")(
      "eps", po::value<long>(&eps)->default_value(5),
      "Epsilon for nonlocal equation")(
      "k", po::value<double>(&k)->default_value(1),
      "Heat transfer coefficient (default: 0.5)")(
      "dt", po::value<double>(&dt)->default_value(0.0005),
      "Timestep unit (default: 1.0[s])")(
      "dh", po::value<double>(&dh)->default_value(0.05),
      "Quantization across space")("no-header",
                                   "do not print out the csv header row");

  // Initialize and run HPX
  return hpx::init(desc_commandline, argc, argv);
}


#include <boost/rational.hpp>

using namespace std;

int main(int argc, char *argv[])
{
  cout << boost::gcd(3,4) << endl;
  cout << boost::lcm(2,6) << endl;

  return 0;
}


//////////////////////////////////////////////////////////////////////////////
///   Copyright 2003 and onward LASMEA UMR 6602 CNRS/U.B.P Clermont-Ferrand
///   Copyright 2009 and onward LRI    UMR 8623 CNRS/Univ Paris Sud XI
///
///          Distributed under the Boost Software License, Version 1.0
///                 See accompanying file LICENSE.txt or copy at
///                     http://www.boost.org/LICENSE_1_0.txt
//////////////////////////////////////////////////////////////////////////////
#ifndef NT2_TOOLBOX_BOOST_MATH_FUNCTION_EXPINT_HPP_INCLUDED
#define NT2_TOOLBOX_BOOST_MATH_FUNCTION_EXPINT_HPP_INCLUDED
#include <nt2/include/simd.hpp>
#include <nt2/include/functor.hpp>
#include <boost/math/special_functions/expint.hpp>

namespace nt2 { namespace boost_math { namespace tag
  {
    struct expint_ {};
  }
  NT2_FUNCTION_IMPLEMENTATION(boost_math::tag::expint_, expint, 1)
  NT2_FUNCTION_IMPLEMENTATION(boost_math::tag::expint_, ei, 1)
  NT2_FUNCTION_IMPLEMENTATION(boost_math::tag::expint_, expint, 2)
  NT2_FUNCTION_IMPLEMENTATION(boost_math::tag::expint_, en, 2)
  } }

#include <nt2/toolbox/boost_math/function/scalar/expint.hpp>
// #include NT2_BOOST_MATH_INCLUDE(expint.hpp)

#endif

// modified by jt the 29/12/2010


//
// Created by eliane on 23/12/18.
//

#include "GradientDescentLineSearch.h"

#include <Engines/ScoringFunctions/VinaLikeRigid.h>

#include <Engines/Internals/InternalsUtilityFunctions.h>

#include "LocalOptimizerUtilityFunctions.h"

#undef BOOST_LOG

#include <boost/log/trivial.hpp>

namespace SmolDock::Optimizer {

    GradientDescentLineSearch::GradientDescentLineSearch(Score::ScoringFunction* scoringFunc_,
                                                         double differentialUpsilon) :
            scoringFunction(scoringFunc_),
            differential_epsilon(differentialUpsilon),
            paramVectorDimension(scoringFunc_->getParamVectorDimension()),
            result(scoringFunc_->getParamVectorDimension(), 1, arma::fill::zeros),
            iterationNumber(0) {

    }

    bool GradientDescentLineSearch::optimize(arma::mat startingPoint) {

        arma::mat &paramsToOptimize = startingPoint;
        arma::mat gradient(paramVectorDimension, 1, arma::fill::zeros);

        double score_ = this->scoringFunction->EvaluateWithGradient(paramsToOptimize, gradient);

        BOOST_LOG_TRIVIAL(debug) << "Initial score : " << score_;

        double oldscore;
        double score_progress = 100.0;

        while (score_progress > 0.001) {
            if (!hasNonZeroComponent(gradient)) {
                // Score and all gradient components are zero : cannot decide on a direction,
                // --> Search failure
                BOOST_LOG_TRIVIAL(debug)
                    << "Gradient components are zero : cannot use gradient descent for this starting position.";
                return false;
            }

            // We do a linesearch
            // Aka : we want to know how far in the gradient direction we will go

            double step = this->differential_epsilon / 10;
            double previous_step = 0.0;
            double previous_tentativeNewScore = std::numeric_limits<double>::max();

            // We go further and further in the direction of the gradient, until we are not bettering the score
            while (true) {

                arma::mat tentativeNewParams = paramsToOptimize + (step * gradient);

                double tentativeNewScore = this->scoringFunction->Evaluate(tentativeNewParams);

                double initial_tentative_scoreDifference = score_ - tentativeNewScore;
                double previous_tentative_scoreDifference = previous_tentativeNewScore - tentativeNewScore;

                // Prepare for next iteration
                previous_tentativeNewScore = tentativeNewScore;

                if (initial_tentative_scoreDifference > 0
                    && previous_tentative_scoreDifference > 0) {
                    // We are both better than the initial score, and the previous try for step
                    // We try a somewhat larger step
                    previous_step = step;
                    step = step + (this->differential_epsilon / 10);
                } else {
                    // We are not better on both count
                    break; // We stop the linesearch
                }
            }

            // Analysing the result of the linesearch
            if (previous_step == 0.0) {
                // We have not been able to do any step which improves the score
                // Most probably, it means we are at a (local) minimum
                break; // Stopping the gradient search
            }

            // Else :
            // The previous step was an improvement, but this one is not
            // We use previous_step

            arma::mat newParams = paramsToOptimize + (previous_step * gradient);
            paramsToOptimize = newParams;

            oldscore = score_;
            score_ = this->scoringFunction->EvaluateWithGradient(paramsToOptimize, gradient);
            this->iterationNumber++;
            score_progress = oldscore - score_;
            BOOST_LOG_TRIVIAL(debug) << "New score: " << score_ << " (progress: " << score_progress << " )";
        }

        BOOST_LOG_TRIVIAL(debug) << "Finished gradient descent";
        this->score = score_;
        result = paramsToOptimize;
        return true;
    }

    unsigned int GradientDescentLineSearch::getIterationNumber() {
        return this->iterationNumber;
    }

    arma::mat GradientDescentLineSearch::getRawResultMatrix() {
        return this->result;
    }

    double GradientDescentLineSearch::getScore() {
        return this->score;
    }

}


#include <string>
#include <sstream>
#include <iostream>
#include <map>
#include <regex>
#include <fstream>
#include <boost/algorithm/string.hpp>

#include "../common.hpp"

using namespace std;

// search updates `found` to contain all the bags that can directly or indirectly contain the target.
// bags assumes a child->parent maping.
void search(set<string> * found, const map<string, map<string, int>> & bags, string target) {
    // Bag doesn't exist :/
    if (bags.find(target) == bags.end()) {
        return;
    }

    for (auto const& [parent, count] : bags.at(target)) {
        if (found->find(parent) != found->end()) {
            continue;
        }

        found->insert(parent);
        search(found, bags, parent);
    }
}

// search2 returns the count of bags within the target bag.
// bags assumes a parent->child mapping.
int search2(const map<string, map<string, int>> & bags, string target) {
    // Bag doesn't exist, so assume it contains nothing.
    if (bags.find(target) == bags.end()) {
        return 1;
    }

    int answer = 1;
    for (auto const& [child, count] : bags.at(target)) {
        answer += count * search2(bags, child);
    }

    return answer;
}

int main() {
    ifstream file ("2020/7.txt");
    if (!file.is_open()) {
        cout << "Failed to open file: " << strerror(errno) << endl;
        return -1;
    }

    int answer1 = 0;
    int answer2 = 0;

    map<string, map<string, int>> bags;
    map<string, map<string, int>> bags2;

    string line;
    while (getline(file, line, '\n')) {
        boost::trim(line);

        if (line == "") {
            continue;
        }

        smatch matches;
        regex e1 ("(.+) bags contain");
        assert(regex_search(line, matches, e1));

        string parent = matches[1];

        std::regex e2("(\\d+) (.+?) bag");
        auto bags_begin = std::sregex_iterator(line.begin(), line.end(), e2);
        auto bags_end = std::sregex_iterator();

        for (std::sregex_iterator i = bags_begin; i != bags_end; ++i) {
            smatch match = *i;
            int count = stoi(match[1]);
            string child = match[2];

            bags[child][parent] = count;
            bags2[parent][child] = count;
        }
    }

    set<string> found;
    search(&found, bags, "shiny gold");

    answer1 = found.size();
    cout << "Answer 7.1: " << answer1 << endl;

    answer2 = search2(bags2, "shiny gold") - 1;
    cout << "Answer 7.2: " << answer2 << endl;

    file.close();
}


#include "src/config.hpp"
#include "src/data.hpp"
#include "src/input.hpp"
#include "src/logging.hpp"
#include "src/output.hpp"
#include "src/physical.hpp"
#include "src/random.hpp"
#include "src/random_body.hpp"
#include "src/space.hpp"
#include "src/tree.hpp"
#include <algorithm>
#include <array>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/mpi.hpp>
#include <boost/optional.hpp>
#include <boost/program_options.hpp>
#include <boost/serialization/nvp.hpp>
#include <cfenv>
#include <cmath>
#include <fenv.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <random>

namespace mpi = boost::mpi;
namespace po = boost::program_options;
namespace logging = n_body::logging;
namespace fs = std::filesystem;

using Number = double;

using boost::archive::xml_oarchive;
using n_body::logging::Level;
using n_body::logging::logger;
using std::array;
using std::cout;
using std::endl;
using std::size_t;
using std::string;

constexpr size_t DIMENSION = 3;
constexpr int ROOT = 0;

namespace n_body {

int main(int argc, char *argv[]) {
  // enable overflow check
  std::feclearexcept(FE_ALL_EXCEPT);
  feenableexcept(FE_DIVBYZERO | FE_UNDERFLOW | FE_OVERFLOW | FE_INVALID);

  mpi::environment env(argc, argv, false);
  mpi::communicator world;
  mpi::timer timer;

  config::Configuration<Number> config;
  po::options_description description("options");
  if (world.rank() == ROOT) {
    description.add_options()("help,h", "print help message");
    description.add_options()("number,n", po::value<unsigned>(),
                              "number of bodies");
    description.add_options()("steps,s",
                              po::value<unsigned>()->default_value(100),
                              "simulate steps");
    description.add_options()("sample-interval",
                              po::value<unsigned>()->default_value(10),
                              "Sample interval");
    description.add_options()("time,t", po::value<Number>()->default_value(1),
                              "time of every single step(s)");
    description.add_options()("gravitational-constant,G",
                              po::value<Number>()->default_value(1),
                              "the gravitational constant(m^3kg^-1s^-2)");
    description.add_options()("theta,p",
                              po::value<Number>()->default_value(1),
                              "Barnes-Hut approximation parameter");
    description.add_options()(
        "output,o", po::value<string>()->default_value("n-body-output"),
        "output directory");
    description.add_options()("input,i", po::value<string>(),
                              "input bodies file");
    description.add_options()(
        "min-log-level,m",
        po::value<logging::Level>()->default_value(logging::Level::Info),
        "Minimal log level");
    description.add_options()("soften-length",
                              po::value<Number>()->default_value(0),
                              "Soften length parameter");

    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, description), vm);
    po::notify(vm);

    if (vm.count("help"))
      config.show_help = true;
    if (vm.count("number")) {
      config.number = vm["number"].as<unsigned>();
    } else {
      config.number = boost::none;
    }
    config.steps = vm["steps"].as<unsigned>();
    config.sample_interval = vm["sample-interval"].as<unsigned>();
    config.time = vm["time"].as<Number>();
    config.G = vm["gravitational-constant"].as<Number>();
    config.theta = vm["theta"].as<Number>();
    config.soften_length = vm["soften-length"].as<Number>();
    if (vm.count("input")) {
      config.input_file = vm["input"].as<string>();
    } else {
      config.input_file = boost::none;
    }
    config.output_path = vm["output"].as<string>();
    config.min_log_level = vm["min-log-level"].as<logging::Level>();
  }
  mpi::broadcast(world, config, ROOT);

  if (config.show_help) {
    if (world.rank() == ROOT) {
      cout << description << std::endl;
    }
    return 0;
  }

  // setup logger
  logging::Configuration::instance().default_communicator = &world;
  logging::Configuration::instance().timer = &timer;
  logging::Configuration::instance().min_level = config.min_log_level;

  if (world.rank() == ROOT) {
    boost::archive::xml_oarchive(logging::logger(logging::Level::Info)
                                     << "dump configuration\n",
                                 boost::archive::no_header)
        << boost::serialization::make_nvp("configuration", config);

    if (config.input_file && config.number) {
      logger(Level::Error) << "input file and number options should not be "
                              "specified simultaneously"
                           << std::endl;
      world.abort(MPI_ERR_ARG);
    }

    if (!config.input_file && !config.number) {
      logger(Level::Error) << "please spcify either body number or input file" << std::endl;
      world.abort(MPI_ERR_ARG);
    }
  }

  boost::optional<fs::path> output_path;
  if (world.rank() == ROOT) {
    output_path = fs::path(config.output_path);
    if (fs::exists(*output_path)) {
      if (!fs::is_directory(*output_path)) {
        logger(Level::Error) << "output path " << *output_path
                             << " is not a directory" << std::endl;
        world.abort(MPI_ERR_ARG);
      }
    }
    fs::create_directories(config.output_path);

    // dump configuration to output path
    output::dump_configuration(*output_path, config);
    output::output_time_information(*output_path, config);
    output::output_sample_number(*output_path, config);
  }

  boost::optional<std::ifstream> infile = boost::none;
  if (world.rank() == ROOT && config.input_file) {
    infile = std::ifstream(*config.input_file);
  }

  data::Bodies<Number, DIMENSION> bodies;
  if (infile) {
    input::input_bodies(world, ROOT, *infile, bodies);
    config.number = bodies.size();
  }

  if (*config.number % world.size() != 0) {
    logger(Level::Error) << "number of bodies(" << *config.number
                         << ") must be divisible by number of processes("
                         << world.size() << ")" << std::endl;
    world.abort(MPI_ERR_ARG);
  }

  if (!infile) {
    random::MinimunStandardEngine random_engine(world, ROOT);
    random::body::BodyGenerator<Number, DIMENSION> body_generator =
        [&](std::size_t i) {
          auto min = static_cast<Number>(-10) * *config.number;
          auto max = static_cast<Number>(10) * *config.number;
          data::Body<Number, DIMENSION> body{};
          for (std::size_t d = 0; d < DIMENSION; ++d) {
            body.position[d] =
                std::uniform_real_distribution<Number>(min, max)(random_engine);
            body.velocity[d] = 0;
          }
          body.mass =
              std::uniform_real_distribution<Number>(0.5, 1)(random_engine);
          return body;
        };
    random::body::random_bodies(world, body_generator, bodies, *config.number);
  }

  if (world.rank() == ROOT) {
    output::dump_bodies(*output_path, bodies);
  }

  constexpr Number INF = std::numeric_limits<Number>::infinity();
  data::Space<Number, DIMENSION> bounds{
      .min =
          {
              INF,
              INF,
              INF,
          },
      .max =
          {
              -INF,
              -INF,
              -INF,
          },
      .center =
          {
              0,
              0,
              0,
          },
  };
  std::size_t output_index = 0;
  if (world.rank() == ROOT) {
    output::output_positions(*output_path, output_index, bodies);
    logger(Level::Info) << "output initial step with index " << output_index
                        << " finished" << endl;
    ++output_index;
  }
  for (decltype(config.steps) s = 0; s < config.steps;) {
    auto root_space = space::root_space(world, bodies);
    space::extend_to_contain(bounds, root_space);
    auto body_tree = data::tree::build_tree(world, root_space, bodies);
    physical::step(config, world, bodies, body_tree);

    ++s;

    if (world.rank() == ROOT && s % config.sample_interval == 0) {
      // do sample
      output::output_positions(*output_path, output_index, bodies);
      logger(Level::Info) << "output step " << s << " with index "
                          << output_index << " finished" << endl;
      ++output_index;
    }
  }
  space::extend_to_contain(bounds, space::root_space(world, bodies));

  if (world.rank() == ROOT) {
    // save last bodies
    output::dump_bodies_finished(*output_path, bodies);
    output::output_bounds(*output_path, bounds);
  }
  return 0;
}

} // namespace n_body

int main(int argc, char *argv[]) { return n_body::main(argc, argv); }


//==================================================================================================
/*!
  @file

  @copyright 2016 NumScale SAS

  Distributed under the Boost Software License, Version 1.0.
  (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
*/
//==================================================================================================
#ifndef BOOST_SIMD_ARCH_COMMON_SIMD_FUNCTION_NEXTAFTER_HPP_INCLUDED
#define BOOST_SIMD_ARCH_COMMON_SIMD_FUNCTION_NEXTAFTER_HPP_INCLUDED
#include <boost/simd/detail/overload.hpp>

#include <boost/simd/meta/hierarchy/simd.hpp>
#include <boost/simd/function/if_else.hpp>
#include <boost/simd/function/is_equal.hpp>
#include <boost/simd/function/is_greater.hpp>
#include <boost/simd/function/is_less.hpp>
#include <boost/simd/function/dec.hpp>
#include <boost/simd/function/next.hpp>
#include <boost/simd/function/inc.hpp>
#include <boost/simd/function/prev.hpp>

namespace boost { namespace simd { namespace ext
{
   namespace bd = boost::dispatch;
   namespace bs = boost::simd;
   BOOST_DISPATCH_OVERLOAD_IF(nextafter_
                          , (typename A0, typename X)
                          , (detail::is_native<X>)
                          , bd::cpu_
                          , bs::pack_<bd::arithmetic_<A0>, X>
                          , bs::pack_<bd::arithmetic_<A0>, X>
                          )
   {
      BOOST_FORCEINLINE A0 operator()( const A0& a0, const A0& a1) const BOOST_NOEXCEPT
      {
        return  if_else(bs::is_equal(a0,a1),
                      a0,
                        if_else(is_greater(a1,a0),saturated_(inc)(a0),saturated_(dec)(a0)));
      }
   };

   BOOST_DISPATCH_OVERLOAD_IF(nextafter_
                          , (typename A0, typename X)
                          , (detail::is_native<X>)
                          , bd::cpu_
                          , bs::pack_<bd::floating_<A0>, X>
                          , bs::pack_<bd::floating_<A0>, X>
                          )
   {
      BOOST_FORCEINLINE A0 operator()( const A0& a0, const A0& a1) const BOOST_NOEXCEPT
      {
        return if_else(is_less(a0, a1), next(a0), if_else(is_equal(a0, a1),  a0, prev(a0)));
      }
   };

} } }

#endif


//==================================================================================================
/*!
  @file

    @copyright 2016 NumScale SAS

  Distributed under the Boost Software License, Version 1.0.
  (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
*/
//==================================================================================================
#ifndef BOOST_SIMD_FUNCTION_CTZ_HPP_INCLUDED
#define BOOST_SIMD_FUNCTION_CTZ_HPP_INCLUDED

#if defined(DOXYGEN_ONLY)
namespace boost { namespace simd
{

  /*!
    @ingroup group-bitwise
    This function object returns the bit count of trailing zeroes.

   @par Header <boost/simd/function/ctz.hpp>

   @par Notes

    - It finds the first bit set in x, and returns the index of that bit, 0 being
    the least significant bit index.

    - @c ctz asserts is the entry is 0

    @see  clz, popcnt

    @par Header <boost/simd/function/ctz.hpp>

    @par Example:

      @snippet ctz.cpp ctz

    @par Possible output:

      @snippet ctz.txt ctz

  **/
  as_integer_t<Value> ctz(Value const& x);
} }
#endif

#include <boost/simd/function/scalar/ctz.hpp>
#include <boost/simd/function/simd/ctz.hpp>

#endif


// Copyright University of Warwick 2014
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

// Authors:
// John Back

/*! \file LpcLineFitter.hh
    \brief File containing the declaration of the LpcLineFitter class
*/

/*! \class LpcLineFitter
    \brief Class that calculates the regression best fit line through a set of data points
*/

#ifndef LPC_LINE_FITTER_HH
#define LPC_LINE_FITTER_HH

#include <Eigen/Dense>

class LpcLineFitter {

public:

    //! Constructor
    LpcLineFitter();

    //! Destructor
    virtual ~LpcLineFitter();

    //! Find the centroid and unit direction vector for the line
    /*!
      \param [in] data The Eigen MatrixXd of the data (row = hit, col = x,y,z,.. coords)
      \returns an STL pair of the centroid and unit direction vector
    */
    std::pair<Eigen::VectorXd, Eigen::VectorXd> findLine(const Eigen::MatrixXd& data) const;

protected:

private:

};

#endif


/*
 * Copyright (c) 2013, Christian Gehring, Hannes Sommer, Paul Furgale, Remo Diethelm
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Autonomous Systems Lab, ETH Zurich nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Christian Gehring, Hannes Sommer, Paul Furgale,
 * Remo Diethelm BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
*/

#pragma once

#include <cmath>

#include <Eigen/Geometry>

#include "kindr/common/common.hpp"
#include "kindr/common/assert_macros_eigen.hpp"
#include "kindr/rotations/RotationBase.hpp"

namespace kindr {

/*! \class RotationVector
 * \brief Implementation of a rotation vector based on Eigen::Matrix<Scalar, 3, 1>
 *
 *  The rotation vector is a non-normalized three-dimensional vector.
 *  The direction of the vector specifies the rotation axis and the length the angle.
 *  This representation uses therefore three parameters.
 *  \see AngleAxis for an angle-axis representation with four parameters.
 *
 *  The following four typedefs are provided for convenience:
 *   - \ref RotationVectorD "RotationVectorAD" for primitive type double
 *   - \ref RotationVectorF "RotationVectorAF" for primitive type float
 *
 *  \tparam PrimType_ the primitive type of the data (double or float)
 *  \ingroup rotations
 */
template<typename PrimType_>
class RotationVector : public RotationBase<RotationVector<PrimType_>> {
 private:
  /*! \brief The base type.
   */
  typedef Eigen::Matrix<PrimType_, 3, 1> Base;

  /*! Data container
   */
  Base vector_;

 public:
  /*! \brief The implementation type.
   *  The implementation type is always an Eigen object.
   */
  typedef Base Implementation;
  /*! \brief The primitive type.
   *  Float/Double
   */
  typedef PrimType_ Scalar;

  /*! \brief Rotation Vector as 3x1-matrix
   */
  typedef Base Vector;

  /*! \brief Default constructor using identity rotation.
   */
  RotationVector()
    : vector_(Base::Zero()) {
  }

  /*! \brief Constructor using three scalars.
   *
   *  \param x      first entry of the rotation vector
   *  \param y      second entry of the rotation vector
   *  \param z      third entry of the rotation vector
   */
  RotationVector(Scalar x, Scalar y, Scalar z)
    : vector_(x,y,z) {
  }

  /*! \brief Constructor using Eigen::Matrix<Scalar, 3, 1>.
   *
   *  \param other   Eigen::Matrix<Scalar, 3, 1>
   */
  explicit RotationVector(const Base& other) // explicit on purpose
    : vector_(other) {
  }

  /*! \brief Constructor using another rotation.
   *  \param other   other rotation
   */
  template<typename OtherDerived_>
  inline explicit RotationVector(const RotationBase<OtherDerived_>& other)
    : vector_(internal::ConversionTraits<RotationVector, OtherDerived_>::convert(other.derived()).toImplementation()) {
  }

  /*! \brief Assignment operator using another rotation.
   *  \param other   other rotation
   *  \returns reference
   */
  template<typename OtherDerived_>
  RotationVector& operator =(const RotationBase<OtherDerived_>& other) {
    this->toImplementation() = internal::ConversionTraits<RotationVector, OtherDerived_>::convert(other.derived()).toImplementation();
    return *this;
  }

  /*! \brief Parenthesis operator to convert from another rotation.
   *  \param other   other rotation
   *  \returns reference
   */
  template<typename OtherDerived_>
  RotationVector& operator ()(const RotationBase<OtherDerived_>& other) {
    this->toImplementation() = internal::ConversionTraits<RotationVector, OtherDerived_>::convert(other.derived()).toImplementation();
    return *this;
  }

  /*! \brief Returns the inverse of the rotation.
   *  \returns the inverse of the rotation
   */
  RotationVector inverted() const {
    return RotationVector(-this->toImplementation());
  }

  /*! \brief Inverts the rotation.
   *  \returns reference
   */
  RotationVector& invert() {
    *this = inverted();
    return *this;
  }

  /*! \brief Cast to the implementation type.
   *  \returns the implementation for direct manipulation (recommended only for advanced users)
   */
  inline Implementation& toImplementation() {
    return static_cast<Implementation&>(vector_);
  }

  /*! \brief Cast to the implementation type.
   *  \returns the implementation for direct manipulation (recommended only for advanced users)
   */
  inline const Implementation& toImplementation() const {
    return static_cast<const Implementation&>(vector_);
  }

  /*! \brief Sets the rotation to identity.
   *  \returns reference
   */
  RotationVector& setIdentity() {
    vector_ = Base::Zero();
    return *this;
  }

  /*! \brief Returns the rotation vector.
   *  \returns the rotation vector (scalar)
   */
  inline const Vector vector() const {
    return this->toImplementation();
  }

  /*! \brief Sets the rotation vector.
   * \param x   first entry
   * \param y   second entry
   * \param z   third entry
   */
  inline void setVector(Scalar x, Scalar y, Scalar z) {
    vector_ << x, y, z;
  }

  /*! \brief Sets the rotation vector.
   */
  inline void setVector(const Implementation& vector) {
    vector_ = vector;
  }

  /*! \brief Returns the first entry of the rotation vector.
   *  \returns first entry of the rotation vector (scalar)
   */
  inline Scalar x() const {
    return vector_(0);
  }

  /*! \brief Returns the second entry of the rotation vector.
   *  \returns second entry of the rotation vector (scalar)
   */
  inline Scalar y() const {
    return vector_(1);
  }

  /*! \brief Returns the third entry of the rotation vector.
   *  \returns third entry of the rotation vector (scalar)
   */
  inline Scalar z() const {
    return vector_(2);
  }

  /*! \brief Sets the first entry of the rotation vector.
   */
  inline void setX(Scalar x) {
    vector_(0) = x;
  }

  /*! \brief Sets the second entry of the rotation vector.
   */
  inline void setY(Scalar y) {
    vector_(1) = y;
  }

  /*! \brief Sets the third entry of the rotation vector.
   */
  inline void setZ(Scalar z) {
    vector_(2) = z;
  }

  /*! \brief Returns a unique rotation vector with norm in [0,pi).
   *  This function is used to compare different rotations.
   *  \returns copy of the rotation vector which is unique
   */
  RotationVector getUnique() const {
    AngleAxis<PrimType_> angleAxis(*this);
    RotationVector rotationVector(angleAxis.getUnique());
    return rotationVector;
  }

  /*! \brief
   *  \returns reference
   */
  RotationVector& setUnique() {
    *this = getUnique();
    return *this;
  }

  /*! \brief Concatenation operator.
   *  This is explicitly specified, because Eigen provides also an operator*.
   *  \returns the concatenation of two rotations
   */
  using RotationBase<RotationVector<PrimType_>>::operator*;

  /*! \brief Used for printing the object with std::cout.
   *  \returns std::stream object
   */
  friend std::ostream& operator << (std::ostream& out, const RotationVector& rotationVector) {
    out << rotationVector.toImplementation().transpose();
    return out;
  }
};

//! \brief Active rotation vector with double primitive type
typedef RotationVector<double>  RotationVectorD;
//! \brief Active rotation vector with float primitive type
typedef RotationVector<float>  RotationVectorF;
//! \brief Passive rotation vector with double primitive type
typedef RotationVector<double> RotationVectorPD;
//! \brief Passive rotation vector with float primitive type
typedef RotationVector<float> RotationVectorPF;

namespace internal {

template<typename PrimType_>
class get_scalar<RotationVector<PrimType_>> {
 public:
  typedef PrimType_ Scalar;
};

template<typename PrimType_>
class get_matrix3X<RotationVector<PrimType_>>{
 public:
  typedef int  IndexType;

  template <IndexType Cols>
  using Matrix3X = Eigen::Matrix<PrimType_, 3, Cols>;
};

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Conversion Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
template<typename DestPrimType_, typename SourcePrimType_>
class ConversionTraits<RotationVector<DestPrimType_>, RotationVector<SourcePrimType_>> {
 public:
  inline static RotationVector<DestPrimType_> convert(const RotationVector<SourcePrimType_>& rotationVector) {
    return RotationVector<DestPrimType_>(rotationVector.toImplementation().template cast<DestPrimType_>());
  }
};

template<typename DestPrimType_, typename SourcePrimType_>
class ConversionTraits<RotationVector<DestPrimType_>, AngleAxis<SourcePrimType_>> {
 public:
  inline static RotationVector<DestPrimType_> convert(const AngleAxis<SourcePrimType_>& angleAxis) {
    const AngleAxis<DestPrimType_> angleAxisDest(angleAxis);
    return RotationVector<DestPrimType_>(angleAxisDest.angle()*angleAxisDest.axis());
  }
};

template<typename DestPrimType_, typename SourcePrimType_>
class ConversionTraits<RotationVector<DestPrimType_>, RotationQuaternion<SourcePrimType_>> {
 public:
  inline static RotationVector<DestPrimType_> convert(const RotationQuaternion<SourcePrimType_>& q) {
    using std::acos;
    using std::sqrt;
    using std::atan2;

    const DestPrimType_ imaginaryVectorNormSquared = DestPrimType_(1.0)-q.real()*q.real();

    if (imaginaryVectorNormSquared < internal::NumTraits<SourcePrimType_>::dummy_precision()) {
      if (q.real() > 0.0) {
        return RotationVector<DestPrimType_>(DestPrimType_(2.0)*q.imaginary().template cast<DestPrimType_>());
      }
      else {
        return RotationVector<DestPrimType_>(DestPrimType_(-2.0)*q.imaginary().template cast<DestPrimType_>());
      }
    }
    const DestPrimType_ imaginaryVectorNorm = q.imaginary().norm();
    return RotationVector<DestPrimType_>((DestPrimType_(2.0)*atan2(imaginaryVectorNorm, q.real())/imaginaryVectorNorm*q.imaginary()).template cast<DestPrimType_>());
  }
};

// Generic conversion
template<typename DestPrimType_, typename SourceImplementation_>
class ConversionTraits<RotationVector<DestPrimType_>, SourceImplementation_> {
 public:
  inline static RotationVector<DestPrimType_> convert(const SourceImplementation_& rotation) {
    return RotationVector<DestPrimType_>(AngleAxis<DestPrimType_>(rotation));
  }
};

template<typename DestPrimType_, typename SourcePrimType_>
class ConversionTraits<RotationVector<DestPrimType_>, RotationMatrix<SourcePrimType_>> {
 public:
  inline static RotationVector<DestPrimType_> convert(const RotationMatrix<SourcePrimType_>& rotationMatrix) {
    // Note that Eigen converts a rotation matrix to angle-axis via quaternion.
    return RotationVector<DestPrimType_>(RotationQuaternion<DestPrimType_>(rotationMatrix));

  }
};

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Multiplication Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Rotation Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */

} // namespace internal
} // namespace kindr


//==============================================================================
//         Copyright 2003 - 2012   LASMEA UMR 6602 CNRS/Univ. Clermont II
//         Copyright 2009 - 2012   LRI    UMR 8623 CNRS/Univ Paris Sud XI
//
//          Distributed under the Boost Software License, Version 1.0.
//                 See accompanying file LICENSE.txt or copy at
//                     http://www.boost.org/LICENSE_1_0.txt
//==============================================================================
#include <boost/simd/include/constants/digits.hpp>

#include <nt2/sdk/unit/module.hpp>
#include <nt2/sdk/unit/tests/relation.hpp>
#include <nt2/sdk/unit/tests/type_expr.hpp>

#include "../constant.hpp"

#ifdef BOOST_MSVC
  #pragma warning(disable: 4309) // truncation of constant value
#endif

NT2_TEST_CASE_TPL( digit_value, BOOST_SIMD_TYPES(char) )
{
  NT2_CHECK_CONSTANT(Mten     , -10, T);
  NT2_CHECK_CONSTANT(Mnine    , -9 , T);
  NT2_CHECK_CONSTANT(Meight   , -8 , T);
  NT2_CHECK_CONSTANT(Mseven   , -7 , T);
  NT2_CHECK_CONSTANT(Msix     , -6 , T);
  NT2_CHECK_CONSTANT(Mfive    , -5 , T);
  NT2_CHECK_CONSTANT(Mfour    , -4 , T);
  NT2_CHECK_CONSTANT(Mthree   , -3 , T);
  NT2_CHECK_CONSTANT(Mtwo     , -2 , T);
  NT2_CHECK_CONSTANT(Mone     , -1 , T);
  NT2_CHECK_CONSTANT(Zero     ,  0 , T);
  NT2_CHECK_CONSTANT(One      ,  1 , T);
  NT2_CHECK_CONSTANT(Two      ,  2 , T);
  NT2_CHECK_CONSTANT(Three    ,  3 , T);
  NT2_CHECK_CONSTANT(Four     ,  4 , T);
  NT2_CHECK_CONSTANT(Five     ,  5 , T);
  NT2_CHECK_CONSTANT(Six      ,  6 , T);
  NT2_CHECK_CONSTANT(Seven    ,  7 , T);
  NT2_CHECK_CONSTANT(Eight    ,  8 , T);
  NT2_CHECK_CONSTANT(Nine     ,  9 , T);
  NT2_CHECK_CONSTANT(Ten      , 10 , T);
  NT2_CHECK_CONSTANT(Eleven   , 11 , T);
  NT2_CHECK_CONSTANT(Twelve   , 12 , T);
  NT2_CHECK_CONSTANT(Fifteen  , 15 , T);
  NT2_CHECK_CONSTANT(Twenty   , 20 , T);
  NT2_CHECK_CONSTANT(Hundred  , 100 , T);
  NT2_CHECK_CONSTANT(Thousand , 1000 , T);
}

NT2_TEST_CASE_TPL( fact_value, BOOST_SIMD_TYPES(char) )
{
  NT2_CHECK_CONSTANT(Fact_4    ,        24, T);
  NT2_CHECK_CONSTANT(Fact_5    ,       120, T);
  NT2_CHECK_CONSTANT(Fact_6    ,       720, T);
  NT2_CHECK_CONSTANT(Fact_7    ,      5040, T);
  NT2_CHECK_CONSTANT(Fact_8    ,     40320, T);
  NT2_CHECK_CONSTANT(Fact_9    ,    362880, T);
  NT2_CHECK_CONSTANT(Fact_10   ,   3628800, T);
  NT2_CHECK_CONSTANT(Fact_11   ,  39916800, T);
  NT2_CHECK_CONSTANT(Fact_12   , 479001600, T);
}

NT2_TEST_CASE_TPL( integral_constant, BOOST_SIMD_INTEGRAL_TYPES(char) )
{
  NT2_TEST_EXPR_TYPE((boost::simd::integral_constant<T,42>()), boost::mpl::_, T);

  NT2_TEST_EQUAL( (boost::simd::integral_constant<T,42>()), static_cast<T>(42) );
}


//
//  NormalGenerator.hpp
//  VI.4 Monte Carlo Simulation
//
//  Created by Zhehao Li on 2020/5/4.
//  Copyright © 2020 Zhehao Li. All rights reserved.
//
//  The declaration of Normal random number generator

#ifndef NormalGenerator_hpp
#define NormalGenerator_hpp

#include <boost/random.hpp>
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/normal_distribution.hpp>
#include <boost/random/variate_generator.hpp>

class NormalGenerator{                                  // an abstract base class

public:
    // Accessing functions
    virtual double getNormal() const = 0;               // pure virtual memeber function

};

class BoostNormalGenerator : public NormalGenerator{

private:
    boost::lagged_fibonacci607 rng;                     // random number generator
    boost::normal_distribution<> nor;                   // default => double
    boost::variate_generator<boost::lagged_fibonacci607 &, boost::normal_distribution<>> * myRandom;        // random variable generator

public:
    // Constructors
    BoostNormalGenerator();

    // Destructors
    ~BoostNormalGenerator();

    // Accessing functions
    double getNormal() const;

};

#endif /* NormalGenerator_hpp */


///////////////////////////////////////////////////////////////
//  Copyright 2011 John Maddock. Distributed under the Boost
//  Software License, Version 1.0. (See accompanying file
//  LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt

#include <boost/multiprecision/tommath.hpp>
#include <iostream>

void t1()
{
//[tommath_eg
//=#include <boost/multiprecision/tommath.hpp>
//=#include <iostream>
//=
//=int main()
//={
   boost::multiprecision::tom_int v = 1;

   // Do some arithmetic:
   for(unsigned i = 1; i <= 1000; ++i)
      v *= i;

   std::cout << v << std::endl; // prints 1000!
   std::cout << std::hex << v << std::endl; // prints 1000! in hex format

   try{
      std::cout << std::hex << -v << std::endl; // Ooops! can't print a negative value in hex format!
   }
   catch(const std::runtime_error& e)
   {
      std::cout << e.what() << std::endl;
   }

   try{
      // v is not a 2's complement type, bitwise operations are only supported
      // on positive values:
      v = -v & 2;
   }
   catch(const std::runtime_error& e)
   {
      std::cout << e.what() << std::endl;
   }

//=   return 0;
//=}
//]
}

void t3()
{
//[mp_rat_eg
//=#include <boost/multiprecision/tommath.hpp>
//=#include <iostream>
//=
//=int main()
//={
   using namespace boost::multiprecision;

   tom_rational v = 1;

   // Do some arithmetic:
   for(unsigned i = 1; i <= 1000; ++i)
      v *= i;
   v /= 10;

   std::cout << v << std::endl; // prints 1000! / 10
   std::cout << numerator(v) << std::endl;
   std::cout << denominator(v) << std::endl;

   tom_rational w(2, 3); // Component wise constructor
   std::cout << w << std::endl; // prints 2/3

//=   return 0;
//=}
//]
}

int main()
{
   t1();
   t3();
   return 0;
}