#include "generalized_eigensolver.h"

#include <immintrin.h>
#include <Eigen/Eigen>

#include "gefast/math/cayley.h"
#include "gefast/util/macros.h"

namespace gefast {

namespace {

Eigen::Matrix4d ComposeG(const Matrix38dRowMajor &ray_directions_matrix1,
                         const Matrix38dRowMajor &ray_directions_matrix2,
                         const Matrix38dRowMajor &ray_centers_matrix1,
                         const Matrix38dRowMajor &rcm2_cross_rdm2,
                         const cayley_t &cayley) {
  rotation_t rotation = CayleyToRotationMatrixUnscaled(cayley);

  Matrix38dRowMajor rot_ray_directions_matrix2 =
      rotation * ray_directions_matrix2;
  Matrix38dRowMajor rot_rcm2_cross_rdm2 = rotation * rcm2_cross_rdm2;

  Eigen::Matrix<double, 4, 8, Eigen::RowMajor> g;
#ifdef GEFAST_INTRINSICS_AVAILABLE
  for (auto i = 0; i < 2; ++i) {
    __m256d _a_1 = _mm256_load_pd(ray_centers_matrix1.data() + i * 4);
    __m256d _a_2 = _mm256_load_pd(ray_centers_matrix1.data() + 8 + i * 4);
    __m256d _a_3 = _mm256_load_pd(ray_centers_matrix1.data() + 16 + i * 4);

    __m256d _b_1 = _mm256_load_pd(rot_ray_directions_matrix2.data() + i * 4);
    __m256d _b_2 =
        _mm256_load_pd(rot_ray_directions_matrix2.data() + 8 + i * 4);
    __m256d _b_3 =
        _mm256_load_pd(rot_ray_directions_matrix2.data() + 16 + i * 4);

    __m256d _rdm1_1 = _mm256_load_pd(ray_directions_matrix1.data() + i * 4);
    __m256d _rdm1_2 = _mm256_load_pd(ray_directions_matrix1.data() + 8 + i * 4);
    __m256d _rdm1_3 =
        _mm256_load_pd(ray_directions_matrix1.data() + 16 + i * 4);

    // ray_centers_matrix1 cross rot_ray_directions_matrix2
    __m256d _res1 = _mm256_mul_pd(_a_3, _b_2);
    _res1 = _mm256_fmsub_pd(_a_2, _b_3, _res1);

    __m256d _res2 = _mm256_mul_pd(_a_1, _b_3);
    _res2 = _mm256_fmsub_pd(_a_3, _b_1, _res2);

    __m256d _res3 = _mm256_mul_pd(_a_2, _b_1);
    _res3 = _mm256_fmsub_pd(_a_1, _b_2, _res3);

    // rdm1 dot rcm1_cross_rot_rdm2

    __m256d _rdm1_dot_rcm1_cross_rot_rdm2 = _mm256_mul_pd(_rdm1_1, _res1);
    _rdm1_dot_rcm1_cross_rot_rdm2 =
        _mm256_fmadd_pd(_rdm1_2, _res2, _rdm1_dot_rcm1_cross_rot_rdm2);
    _rdm1_dot_rcm1_cross_rot_rdm2 =
        _mm256_fmadd_pd(_rdm1_3, _res3, _rdm1_dot_rcm1_cross_rot_rdm2);

    // rdm1 cross rot_rdm2

    _res1 = _mm256_mul_pd(_rdm1_3, _b_2);
    _res1 = _mm256_fmsub_pd(_rdm1_2, _b_3, _res1);

    _res2 = _mm256_mul_pd(_rdm1_1, _b_3);
    _res2 = _mm256_fmsub_pd(_rdm1_3, _b_1, _res2);

    _res3 = _mm256_mul_pd(_rdm1_2, _b_1);
    _res3 = _mm256_fmsub_pd(_rdm1_1, _b_2, _res3);

    _mm256_store_pd(g.data() + i * 4, _res1);
    _mm256_store_pd(g.data() + 8 + i * 4, _res2);
    _mm256_store_pd(g.data() + 16 + i * 4, _res3);

    // rdm1 dot rot_rcm2_cross_rdm2

    _a_1 = _mm256_load_pd(rot_rcm2_cross_rdm2.data() + i * 4);
    _a_2 = _mm256_load_pd(rot_rcm2_cross_rdm2.data() + 8 + i * 4);
    _a_3 = _mm256_load_pd(rot_rcm2_cross_rdm2.data() + 16 + i * 4);

    __m256d _rdm1_dot_rot_rcm2_cross_rdm2 = _mm256_mul_pd(_rdm1_1, _a_1);
    _rdm1_dot_rot_rcm2_cross_rdm2 =
        _mm256_fmadd_pd(_rdm1_2, _a_2, _rdm1_dot_rot_rcm2_cross_rdm2);
    _rdm1_dot_rot_rcm2_cross_rdm2 =
        _mm256_fmadd_pd(_rdm1_3, _a_3, _rdm1_dot_rot_rcm2_cross_rdm2);

    _mm256_store_pd(g.data() + 24 + i * 4,
                    _mm256_sub_pd(_rdm1_dot_rcm1_cross_rot_rdm2,
                                  _rdm1_dot_rot_rcm2_cross_rdm2));
  }
#else
  for (auto i = 0; i != ray_directions_matrix1.cols(); ++i) {
    g.block<3, 1>(0, i) =
        ray_directions_matrix1.col(i).cross(rot_ray_directions_matrix2.col(i));
    g(3, i) =
        ray_directions_matrix1.col(i).transpose() *
        (ray_centers_matrix1.col(i).cross(rot_ray_directions_matrix2.col(i)) -
         rot_rcm2_cross_rdm2.col(i));
  }
#endif
  return g * g.transpose();
}

eigenvalues_t GetEigenvalues(const Matrix38dRowMajor &ray_directions_matrix1,
                             const Matrix38dRowMajor &ray_directions_matrix2,
                             const Matrix38dRowMajor &ray_centers_matrix1,
                             const Matrix38dRowMajor &rcm2_cross_rdm2,
                             const cayley_t &cayley) {
  Eigen::Matrix4d G = ComposeG(ray_directions_matrix1, ray_directions_matrix2,
                               ray_centers_matrix1, rcm2_cross_rdm2, cayley);

  // now compute the roots in closed-form
  const auto G33 = G(3, 3);
  const auto G22 = G(2, 2);
  const auto G11 = G(1, 1);
  const auto G00 = G(0, 0);
  const auto G12 = G(1, 2);
  const auto G13 = G(1, 3);
  const auto G23 = G(2, 3);
  const auto G01 = G(0, 1);
  const auto G02 = G(0, 2);
  const auto G03 = G(0, 3);
  const auto G11_plus_G22 = G11 + G22;
  const auto G22_plus_G33 = G22 + G33;
  const auto G00_plus_G11 = G00 + G11;
  const auto G01_2 = G01 * G01;
  const auto G02_2 = G02 * G02;
  const auto G03_2 = G03 * G03;
  const auto G12_2 = G12 * G12;
  const auto G13_2 = G13 * G13;
  const auto G23_2 = G23 * G23;
  const auto G12_2_plus_G13_2_plus_G23_3 = G12_2 + G13_2 + G23_2;

  const double B = -(G00_plus_G11 + G22_plus_G33);
  const double C = -G12_2_plus_G13_2_plus_G23_3 + G33 * (G00_plus_G11 + G22) +
                   G11 * G22 - (G03_2 + G02_2 + G01_2) + G00 * (G11_plus_G22);
  const double D =
      G13_2 * G22 -
      2.0 * (G12 * (G13 * G23 + G01 * G02) + G03 * (G02 * G23 + G01 * G13)) +
      G11 * G23_2 - (G22 * G33 * (G00_plus_G11) + G00 * G11 * (G22_plus_G33)) +
      G03_2 * G11_plus_G22 + G02_2 * (G33 + G11) + G01_2 * G22_plus_G33 +
      G00 * (G12_2_plus_G13_2_plus_G23_3) + G12_2 * G33;

  const double E = G.determinant();

  double B_pw2 = B * B;
  double B_pw3 = B_pw2 * B;
  double B_pw4 = B_pw3 * B;
  double alpha = -0.375 * B_pw2 + C;
  double beta = B_pw3 / 8.0 - B * C / 2.0 + D;
  double gamma = -0.01171875 * B_pw4 + B_pw2 * C / 16.0 - B * D / 4.0 + E;
  double alpha_pw2 = alpha * alpha;
  double alpha_pw3 = alpha_pw2 * alpha;
  double p = -alpha_pw2 / 12.0 - gamma;
  double q = -alpha_pw3 / 108.0 + alpha * gamma / 3.0 - beta * beta / 8.0;

  double theta2 = -p / 3.0;
  double theta1 = sqrt(theta2) *
                  cos((1.0 / 3.0) * acos((-q / 2.0) / sqrt(-p * p * p / 27.0)));
  double y = -(5.0 / 6.0) * alpha -
             ((1.0 / 3.0) * p * theta1 - theta1 * theta2) / theta2;
  double w = sqrt(alpha + 2.0 * y);

  // we currently disable the computation of all other roots, they are not used
  double temp1 = -B / 4.0 - 0.5 * w;
  double temp2 = 0.5 * sqrt(-3.0 * alpha - 2.0 * y + 2.0 * beta / w);

  eigenvalues_t roots;
  roots[0] = temp1 + temp2;
  roots[1] = temp1 - temp2;
  return roots;
}

double GetCost(const Matrix38dRowMajor &ray_directions_matrix1,
               const Matrix38dRowMajor &ray_directions_matrix2,
               const Matrix38dRowMajor &ray_centers_matrix1,
               const Matrix38dRowMajor &rcm2_cross_rdm2, const cayley_t &cayley,
               bool use_smallest_ev) {
  Eigen::Vector2d roots =
      GetEigenvalues(ray_directions_matrix1, ray_directions_matrix2,
                     ray_centers_matrix1, rcm2_cross_rdm2, cayley);

  if (use_smallest_ev) return roots[1];
  return roots[0];
}

jacobian_t GetJacobian(const Matrix38dRowMajor &ray_directions_matrix1,
                       const Matrix38dRowMajor &ray_directions_matrix2,
                       const Matrix38dRowMajor &ray_centers_matrix1,
                       const Matrix38dRowMajor &rcm2_cross_rdm2,
                       const cayley_t &cayley, double current_eigenvalue,
                       bool use_smallest_ev) {
  jacobian_t jacobian;
  const double kEpsilon = 0.00000001;

  for (auto j = 0; j != 3; ++j) {
    cayley_t cayley_j = cayley;
    cayley_j(j) += kEpsilon;
    auto cost_j = GetCost(ray_directions_matrix1, ray_directions_matrix2,
                          ray_centers_matrix1, rcm2_cross_rdm2, cayley_j,
                          use_smallest_ev);
    jacobian(j) =
        cost_j - current_eigenvalue;  // division by eps can be ommited
  }
  return jacobian;
}

void FindModel(const Matrix38dRowMajor &ray_directions_matrix1,
               const Matrix38dRowMajor &ray_directions_matrix2,
               const Matrix38dRowMajor &ray_centers_matrix1,
               const Matrix38dRowMajor &rcm2_cross_rdm2,
               const cayley_t &starting_point, RelativePose &model) {
  double lambda = 0.017;
  double lambda_modifier = 2.0;
  const double kMaxLambda = 0.07;
  const double kMinLambda = 0.00001;
  const int kMaxIterations = 11;
  const bool kDisableIncrements = false;

  double disturbance_amplitude = 0.3;
  bool found = false;
  int random_trials = 0;
  const int kMaxRandomTrials = 5;
  cayley_t cayley;

  while (!found && random_trials < kMaxRandomTrials) {
    int iterations = 0;
    if (random_trials > 2) disturbance_amplitude = 0.6;
    cayley.noalias() = starting_point;
    if (random_trials != 0)
      cayley.noalias() += disturbance_amplitude * Eigen::Vector3d::Random();

    double smallestEV =
        GetCost(ray_directions_matrix1, ray_directions_matrix2,
                ray_centers_matrix1, rcm2_cross_rdm2, cayley, true);

    jacobian_t jacobian = GetJacobian(
        ray_directions_matrix1, ray_directions_matrix2, ray_centers_matrix1,
        rcm2_cross_rdm2, cayley, smallestEV, true);
    jacobian.normalize();
    Eigen::Matrix3d inverse_hessian = Eigen::Matrix3d::Identity();

    while (iterations < kMaxIterations) {
      Eigen::Vector3d searchDirection = -inverse_hessian * jacobian;
      searchDirection.normalize();

      if (jacobian.dot(searchDirection) > 0) {
        inverse_hessian = Eigen::Matrix3d::Identity();
        searchDirection = -jacobian;
      }

      lambda = 0.017;
      cayley_t next_cayley = cayley + lambda * searchDirection;

      double nextEV =
          GetCost(ray_directions_matrix1, ray_directions_matrix2,
                  ray_centers_matrix1, rcm2_cross_rdm2, next_cayley, true);

      if (iterations == 0 || !kDisableIncrements) {
        while (nextEV < smallestEV) {
          smallestEV = nextEV;
          if (lambda * lambda_modifier > kMaxLambda) break;
          lambda *= lambda_modifier;
          next_cayley.noalias() = cayley + lambda * searchDirection;
          nextEV =
              GetCost(ray_directions_matrix1, ray_directions_matrix2,
                      ray_centers_matrix1, rcm2_cross_rdm2, next_cayley, true);
        }
      }

      while (nextEV > smallestEV) {
        lambda /= lambda_modifier;
        if (lambda < kMinLambda) break;
        next_cayley = cayley + lambda * searchDirection;
        nextEV =
            GetCost(ray_directions_matrix1, ray_directions_matrix2,
                    ray_centers_matrix1, rcm2_cross_rdm2, next_cayley, true);
      }

      jacobian_t next_jacobian = GetJacobian(
          ray_directions_matrix1, ray_directions_matrix2, ray_centers_matrix1,
          rcm2_cross_rdm2, next_cayley, nextEV, true);
      next_jacobian.normalize();

      Eigen::Vector3d s = lambda * searchDirection;
      Eigen::Vector3d y = next_jacobian - jacobian;
      double rho = 1.0 / (y.dot(s));

      inverse_hessian =
          inverse_hessian -
          rho * (s * (y.transpose() * inverse_hessian) +
                 (inverse_hessian * y) * s.transpose()) +
          rho * (rho * (y).dot(inverse_hessian * y) + 1) * (s * s.transpose());

      cayley = next_cayley;
      smallestEV = nextEV;
      jacobian = next_jacobian;

      if (lambda < kMinLambda) break;
      ++iterations;
    }

    if (cayley.norm() < 0.01) {
      // we are close to the origin, test the EV 2
      double ev2 = GetCost(ray_directions_matrix1, ray_directions_matrix2,
                           ray_centers_matrix1, rcm2_cross_rdm2, cayley, false);
      if (ev2 > 0.001)
        ++random_trials;
      else
        found = true;
    } else
      found = true;
  }

  Eigen::Matrix4d G = ComposeG(ray_directions_matrix1, ray_directions_matrix2,
                               ray_centers_matrix1, rcm2_cross_rdm2, cayley);

  Eigen::EigenSolver<Eigen::Matrix4d> eigensolver_G(G, true);
  Eigen::Vector4d D = eigensolver_G.eigenvalues().real();
  Eigen::Matrix4d V = eigensolver_G.eigenvectors().real();

  auto min_eigenvalue_idx = 0;
  D.minCoeff(&min_eigenvalue_idx);

  model.rotation = CayleyToRotationMatrix(cayley);
  model.translation = V.col(min_eigenvalue_idx).hnormalized();
}

}  // namespace

void SolveGE(std::vector<Eigen::Vector3d> &ray_centers1,
             std::vector<Eigen::Vector3d> &ray_directions1,
             std::vector<Eigen::Vector3d> &ray_centers2,
             std::vector<Eigen::Vector3d> &ray_directions2,
             RelativePose &output) {
  const int kCorrespondencesNumber = static_cast<int>(ray_centers1.size());
  GEFAST_ASSERT(kCorrespondencesNumber == 8,
                "the solver only works with 8 correspondences");
  GEFAST_ASSERT(ray_centers2.size() == kCorrespondencesNumber,
                "the number of second camera ray centers must be 8");
  GEFAST_ASSERT(ray_directions1.size() == kCorrespondencesNumber,
                "the number of first camera ray directions must be 8");
  GEFAST_ASSERT(ray_directions2.size() == kCorrespondencesNumber,
                "the number of second camera ray directions must be 8");

  Matrix38dRowMajor ray_directions_matrix1 = Eigen::Map<Matrix38dColMajor>(
      reinterpret_cast<double *>(ray_directions1.data()));
  Matrix38dRowMajor ray_directions_matrix2 = Eigen::Map<Matrix38dColMajor>(
      reinterpret_cast<double *>(ray_directions2.data()));
  Matrix38dRowMajor ray_centers_matrix1 = Eigen::Map<Matrix38dColMajor>(
      reinterpret_cast<double *>(ray_centers1.data()));
  Matrix38dRowMajor ray_centers_matrix2 = Eigen::Map<Matrix38dColMajor>(
      reinterpret_cast<double *>(ray_centers2.data()));
  Matrix38dRowMajor rcm2_cross_rdm2;

  Eigen::Vector3d points_center1 = ray_directions_matrix1.rowwise().sum();
  Eigen::Vector3d points_center2 = ray_directions_matrix2.rowwise().sum();

#ifdef GEFAST_INTRINSICS_AVAILABLE
  for (auto i = 0; i < kCorrespondencesNumber / 4; ++i) {
    __m256d _rcm2_1 = _mm256_load_pd(ray_centers_matrix2.data() + i * 4);
    __m256d _rcm2_2 = _mm256_load_pd(ray_centers_matrix2.data() + 8 + i * 4);
    __m256d _rcm2_3 = _mm256_load_pd(ray_centers_matrix2.data() + 16 + i * 4);

    __m256d _rdm2_1 = _mm256_load_pd(ray_directions_matrix2.data() + i * 4);
    __m256d _rdm2_2 = _mm256_load_pd(ray_directions_matrix2.data() + 8 + i * 4);
    __m256d _rdm2_3 =
        _mm256_load_pd(ray_directions_matrix2.data() + 16 + i * 4);

    __m256d _res1 = _mm256_mul_pd(_rcm2_3, _rdm2_2);
    _res1 = _mm256_fmsub_pd(_rcm2_2, _rdm2_3, _res1);

    __m256d _res2 = _mm256_mul_pd(_rcm2_1, _rdm2_3);
    _res2 = _mm256_fmsub_pd(_rcm2_3, _rdm2_1, _res2);

    __m256d _res3 = _mm256_mul_pd(_rcm2_2, _rdm2_1);
    _res3 = _mm256_fmsub_pd(_rcm2_1, _rdm2_2, _res3);

    _mm256_store_pd(rcm2_cross_rdm2.data() + i * 4, _res1);
    _mm256_store_pd(rcm2_cross_rdm2.data() + 8 + i * 4, _res2);
    _mm256_store_pd(rcm2_cross_rdm2.data() + 16 + i * 4, _res3);
  }
#else
  for (auto i = 0; i < kCorrespondencesNumber; ++i) {
    rcm2_cross_rdm2.col(i).noalias() =
        ray_centers_matrix2.col(i).cross(ray_directions_matrix2.col(i));
  }
#endif

  points_center1 /= kCorrespondencesNumber;
  points_center2 /= kCorrespondencesNumber;

  Eigen::Matrix3d Hcross =
      (ray_directions_matrix2.colwise() - points_center2) *
      (ray_directions_matrix1.colwise() - points_center1).transpose();

  // SVD decomposition of matrix Hcross to obtain initial rotation
  Eigen::JacobiSVD<Eigen::Matrix3d> svd(
      Hcross, Eigen::ComputeFullU | Eigen::ComputeFullV);

  Eigen::Matrix3d V = svd.matrixV();
  Eigen::Matrix3d U = svd.matrixU();
  rotation_t starting_rotation = V * U.transpose();

  if (starting_rotation.determinant() < 0) {
    Eigen::Matrix3d V_prime;
    V_prime.col(0) = V.col(0);
    V_prime.col(1) = V.col(1);
    V_prime.col(2) = -V.col(2);
    starting_rotation.noalias() = V_prime * U.transpose();
  }

  FindModel(ray_directions_matrix1, ray_directions_matrix2, ray_centers_matrix1,
            rcm2_cross_rdm2, RotationMatrixToCayley(starting_rotation), output);
}

}  // namespace gefast


#include "kondo.h"
#include "iostream_util.h"
#include <cstdio>
#include <sstream>
#include <boost/filesystem.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>

using boost::property_tree::ptree;
using boost::property_tree::read_json;
//using boost::property_tree::write_json;

template <typename T>
std::vector<T> as_vector(ptree const& pt, ptree::key_type const& key)
{
    std::vector<T> r;
    for (auto& item : pt.get_child(key))
        r.push_back(item.second.get_value<T>());
    return r;
}

void triangular(int argc, char *argv[]) {
    auto engine = fkpm::mk_engine<cx_flt>();
    if (engine == nullptr) std::exit(EXIT_FAILURE);
    if (argc != 2) {
        cout << "Usage: " << argv[0] << " <base_dir>\n";
        std::exit(EXIT_SUCCESS);
    }

    std::string base_dir(argv[1]);
    auto input_name = base_dir + "/config.toml";
    std::cout << "using toml file `" << input_name << "`!\n";
    toml_ptr g = toml_from_file(input_name);

    auto m = SimpleModel::mk_triangular(toml_get<int64_t>(g, "model.w"), toml_get<int64_t>(g, "model.h"));
    m->J  = toml_get<double>(g, "model.J");
    m->t1 = toml_get(g, "model.t1", 0.0);
    m->t2 = toml_get(g, "model.t2", 0.0);
    m->t3 = toml_get(g, "model.t3", 0.0);
    m->kT_init  = toml_get<double>(g, "model.kT");
    m->kT_decay = toml_get(g, "model.kT_decay", 0.0);
    m->zeeman   = {toml_get(g, "model.zeeman_x", 0.0), toml_get(g, "model.zeeman_y", 0.0), toml_get(g, "model.zeeman_z", 0.0)};
    m->easy_z   = toml_get(g, "model.easy_z", 0.0);
    m->s1       = toml_get(g, "model.s1", 0.0);
    m->s2       = toml_get(g, "model.s2", 0.0);
    m->s3       = toml_get(g, "model.s3", 0.0);

    std::string json_name;
    std::cout << "Input dumpfile name:" << std::endl;
    std::cin >> json_name;
    std::cout << json_name << std::endl;
    json_name = base_dir + "/dump/" + json_name;
    std::ifstream json_file(json_name);
    if (!json_file.is_open()) {
        cerr << "Unable to open file `" << json_name << "`!\n";
        std::exit(EXIT_FAILURE);
    }
    cout << "Using json file `" << json_name << "`!\n";
    ptree pt_json;        // used for reading the json file
    std::string json_eachline, json_contents;
    while (std::getline(json_file, json_eachline)) {
        json_contents += json_eachline;
    }
    std::istringstream is (json_contents);
    read_json (is, pt_json);
    auto time    = pt_json.get<int>    ("time");
    auto action  = pt_json.get<double> ("action");
    auto filling = pt_json.get<double> ("filling");
    auto mu      = pt_json.get<double> ("mu");
    auto spin    = as_vector<double>(pt_json, "spin");

    std::cout << "lattice: " << toml_get<std::string>(g, "model.lattice") << std::endl;
    std::cout << "time:    " << time << std::endl;
    std::cout << "action:  " << action << std::endl;
    std::cout << "filling: " << filling << std::endl;
    std::cout << "mu:      " << mu << std::endl;
    std::cout << "J:       " << m->J << std::endl;
    std::cout << "t:       " << m->t1 << std::endl;
    std::cout << "kT:      " << m->kT() << std::endl;

    assert(spin.size() == m->n_sites * 3);              // build spin configuration from dump file
    for (int i = 0; i < m->n_sites; i++) {
        m->spin[i] = vec3(spin[3*i],spin[3*i+1],spin[3*i+2]);
    }
    m->set_hamiltonian(m->spin);

    int purpose, M, Mq, use_correlated, n_colors, seed;
    std::cout << "Inut 0/1/2 for calculating sigma_xx/sigma_xy/both: " << std::endl;
    std::cin >> purpose;
    fkpm::EnergyScale es;
    es = engine->energy_scale(m->H, 0.1);
    std::cout << "energyscale: [" << es.lo << ", " << es.hi << "]" << std::endl;
    std::cout << "Input M:" << std::endl;
    std::cin >> M;
    std::cout << M << std::endl;
    std::cout << "Input Mq:" << std::endl;
    std::cin >> Mq;
    std::cout << Mq << std::endl;
    std::cout << "Input 0/1 for using uncorrelated/correlated random numbers:" << std::endl;
    std::cin >> use_correlated;
    std::cout << use_correlated << std::endl;
    std::cout << "Input n_colors:" << std::endl;
    std::cin >> n_colors;
    std::cout << n_colors << std::endl;
    std::cout << "Input seed:" << std::endl;
    std::cin >> seed;
    std::cout << seed << std::endl;

    fkpm::RNG rng(seed);
    if (use_correlated) {
        engine->set_R_correlated(m->groups(n_colors), rng);
    } else {
        engine->set_R_uncorrelated(m->H.n_rows, 2*n_colors, rng);
    }

    auto kernel = fkpm::jackson_kernel(M);
    engine->set_H(m->H, es);

    auto jx = m->electric_current_operator(m->spin, {1,0,0});
    auto jy = m->electric_current_operator(m->spin, {0,1,0});

    json_file.close();

    auto mu_dos = engine->moments(M);
    auto gamma = fkpm::moment_transform(mu_dos, Mq);
    fkpm::Vec<double> mu_list, rho;
    fkpm::density_function(gamma, es, mu_list, rho);
    for (int i = 0; i < rho.size(); i++) {
        rho[i]/=m->n_sites;
    }

    std::cout << "calculating moments2... " << std::flush;
    fkpm::timer[0].reset();
    Vec<Vec<fkpm::cx_double>> mu_xx, mu_xy;
    if (purpose == 0) {
        mu_xx = engine->moments2_v1(M, jx, jx, 10, 16);
        mu_xy = mu_xx; // invalid
    } else if (purpose == 1){
        mu_xy = engine->moments2_v1(M, jx, jy, 10, 16);
        mu_xx = mu_xy; // invalid
    } else {
        mu_xx = engine->moments2_v1(M, jx, jx, 10, 16);
        mu_xy = engine->moments2_v1(M, jx, jy, 10, 16);
    }
    cout << " done. " << fkpm::timer[0].measure() << "s.\n";

    cout << "calculating dc conductivities... " << std::flush;
    std::ofstream fout2("full_time"+std::to_string(time)+"_M"+std::to_string(M)+"_corr"+std::to_string(use_correlated)+
                        "_color"+std::to_string(n_colors)+"_seed"+std::to_string(seed)+".dat", std::ios::out);
    fout2 << std::scientific << std::right;
    fout2 << std::setw(20) << "#(1)" << std::setw(20) << "(2)" << std::setw(20) << "(3)"
          << std::setw(20) << "(4)" << std::setw(20) << "(5)" << std::setw(20) << "(6)" << std::endl;
    fout2 << std::setw(20) << "M" << std::setw(20) << "kT" << std::setw(20) << "mu"
          << std::setw(20) << "rho" << std::setw(20) << "sigma_xx" << std::setw(20) << "sigma_xy" << std::endl;
    int interval = std::max(Mq/400,5);
    for (int i = 0; i < Mq; i+=interval) {
        auto cmn = electrical_conductivity_coefficients(M, Mq, m->kT(), mu_list[i], 0.0, es, kernel);
        auto sigma_xx = std::real(fkpm::moment_product(cmn, mu_xx));
        auto sigma_xy = std::real(fkpm::moment_product(cmn, mu_xy));
        fout2 << std::setw(20) << M << std::setw(20) << m->kT() << std::setw(20) << mu_list[i]
              << std::setw(20) << rho[i] << std::setw(20) << sigma_xx << std::setw(20) << sigma_xy << std::endl;
    }
    fout2.close();
    cout << " done. " << fkpm::timer[0].measure() << "s.\n";

    std::ifstream fin0("result_time"+std::to_string(time)+".dat");
    if (! fin0.good()) {
        fin0.close();
        std::ofstream fout3("result_time"+std::to_string(time)+".dat", std::ios::out | std::ios::app );
        fout3 << std::setw(20) << "#(1)" << std::setw(20) << "(2)" << std::setw(20) << "(3)"
              << std::setw(20) << "(4)" << std::setw(20) << "(5)" << std::setw(20) << "(6)"
              << std::setw(20) << "(7)" << std::setw(20) << "(8)" << std::endl;
        fout3 << std::setw(20) << "M" << std::setw(20) << "correlated" << std::setw(20) << "colors"
              << std::setw(20) << "seed" << std::setw(20) << "F" << std::setw(20) << "mu"
              << std::setw(20) << "sigma_xx" << std::setw(20) << "sigma_xy" << std::endl;
        fout3.close();
    }
    std::ofstream fout3("result_time"+std::to_string(time)+".dat", std::ios::out | std::ios::app );
    fout3 << std::scientific << std::right;
    auto cmn       = electrical_conductivity_coefficients(M, Mq, m->kT(), mu, 0.0, es, kernel);

    fout3 << std::setw(20) << M << std::setw(20) << use_correlated << std::setw(20) << n_colors
          << std::setw(20) << seed
          << std::setw(20) << fkpm::electronic_energy(gamma, es, m->kT(), filling, mu)/m->n_sites
          << std::setw(20) << mu
          << std::setw(20) << std::real(fkpm::moment_product(cmn, mu_xx))
          << std::setw(20) << std::real(fkpm::moment_product(cmn, mu_xy))
          << std::endl;
    fout3.close();
    std::cout << std::endl;
    std::remove("dump_device0.dat");
    std::remove("dump_device1.dat");
}

int main(int argc, char *argv[]) {
    triangular(argc, argv);
}


// --------------------------------------------------------------------------
//                   OpenMS -- Open-Source Mass Spectrometry
// --------------------------------------------------------------------------
// Copyright The OpenMS Team -- Eberhard Karls University Tuebingen,
// ETH Zurich, and Freie Universitaet Berlin 2002-2017.
//
// This software is released under a three-clause BSD license:
//  * 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 any author or any participating institution
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission.
// For a full list of authors, refer to the file AUTHORS.
// --------------------------------------------------------------------------
// 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 ANY OF THE AUTHORS OR THE CONTRIBUTING
// INSTITUTIONS 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.
//
// --------------------------------------------------------------------------
// $Maintainer: Timo Sachsenberg $
// $Authors: Clemens Groepl, Chris Bielow $
// --------------------------------------------------------------------------

#include <boost/math/distributions/cauchy.hpp>
#include <OpenMS/TRANSFORMATIONS/FEATUREFINDER/IsotopeModel.h>
#include <OpenMS/MATH/STATISTICS/BasicStatistics.h>
#include <OpenMS/CHEMISTRY/EmpiricalFormula.h>

namespace OpenMS
{
  IsotopeModel::IsotopeModel() :
    InterpolationModel(),
    charge_(0),
    monoisotopic_mz_(0.0)
  {
    setName(getProductName());

    defaults_.setValue("averagines:C", 0.04443989f, "Number of C atoms per Dalton of mass.", ListUtils::create<String>("advanced"));
    defaults_.setValue("averagines:H", 0.06981572f, "Number of H atoms per Dalton of mass.", ListUtils::create<String>("advanced"));
    defaults_.setValue("averagines:N", 0.01221773f, "Number of N atoms per Dalton of mass.", ListUtils::create<String>("advanced"));
    defaults_.setValue("averagines:O", 0.01329399f, "Number of O atoms per Dalton of mass.", ListUtils::create<String>("advanced"));
    defaults_.setValue("averagines:S", 0.00037525f, "Number of S atoms per Dalton of mass.", ListUtils::create<String>("advanced"));
    defaults_.setValue("isotope:trim_right_cutoff", 0.001, "Cutoff in averagine distribution, trailing isotopes below this relative intensity are not considered.", ListUtils::create<String>("advanced"));
    defaults_.setValue("isotope:maximum", 100, "Maximum isotopic rank to be considered.", ListUtils::create<String>("advanced"));
    defaults_.setValue("isotope:distance", 1.000495, "Distance between consecutive isotopic peaks.", ListUtils::create<String>("advanced"));

    defaults_.setValue("isotope:mode:mode", "Gaussian", "Peak Shape used around each isotope peak.", ListUtils::create<String>("advanced"));
    defaults_.setValidStrings("isotope:mode:mode", ListUtils::create<String>("Gaussian,Lorentzian"));
    defaults_.setValue("isotope:mode:LorentzFWHM", 0.3, "Full width of the Lorentzian (Cauchy) function applied to the averagine isotopic pattern to simulate the inaccuracy of the mass spectrometer.", ListUtils::create<String>("advanced"));
    defaults_.setValue("isotope:mode:GaussianSD", 0.1, "Standard deviation of Gaussian applied to the averagine isotopic pattern to simulate the inaccuracy of the mass spectrometer.", ListUtils::create<String>("advanced"));

    defaults_.setValue("charge", 1, "Charge state of the model.", ListUtils::create<String>("advanced"));
    defaults_.setValue("statistics:mean", 0.0, "Centroid m/z (as opposed to monoisotopic m/z).", ListUtils::create<String>("advanced"));

    defaultsToParam_();
  }

  IsotopeModel::IsotopeModel(const IsotopeModel & source) :
    InterpolationModel(source)
  {
    setParameters(source.getParameters());
    updateMembers_();
  }

  IsotopeModel::~IsotopeModel()
  {
  }

  IsotopeModel & IsotopeModel::operator=(const IsotopeModel & source)
  {
    if (&source == this)
      return *this;

    InterpolationModel::operator=(source);
    setParameters(source.getParameters());
    updateMembers_();

    return *this;
  }

  EmpiricalFormula IsotopeModel::getFormula()
  {
    CoordinateType mass = mean_ * charge_;

    Int C_num = Int(0.5 + mass * averagine_[C]);
    Int N_num = Int(0.5 + mass * averagine_[N]);
    Int O_num = Int(0.5 + mass * averagine_[O]);
    Int H_num = Int(0.5 + mass * averagine_[H]);
    Int S_num = Int(0.5 + mass * averagine_[S]);

    String form;
    if (C_num)
      form.append("C").append(String(C_num));
    if (H_num)
      form.append("H").append(String(H_num));
    if (N_num)
      form.append("N").append(String(N_num));
    if (O_num)
      form.append("O").append(String(O_num));
    if (S_num)
      form.append("S").append(String(S_num));

    return EmpiricalFormula(form);
  }

  const IsotopeDistribution & IsotopeModel::getIsotopeDistribution() const
  {
    return isotope_distribution_;
  }

  void IsotopeModel::setSamples(const EmpiricalFormula & formula)
  {
    typedef std::vector<double> ContainerType;
    ContainerType isotopes_exact;

    isotope_distribution_ = formula.getIsotopeDistribution(max_isotope_);

    isotope_distribution_.trimRight(trim_right_cutoff_);
    isotope_distribution_.renormalize();

    // compute the average mass (-offset)
    CoordinateType isotopes_mean = 0;
    {
      Int cnt = 0;
      for (IsotopeDistribution::iterator iter = isotope_distribution_.begin();
           iter != isotope_distribution_.end(); ++iter, ++cnt)
      {
        isotopes_exact.push_back(iter->second);
        isotopes_mean += iter->second * cnt;
      }
      isotopes_mean *= isotope_distance_ / charge_;
    }
    // (Need not divide by sum of probabilities, which is 1.)

    ///
    // "stretch" the averagine isotope distribution (so we can add datapoints between isotope peaks)
    ///
    size_t isotopes_exact_size = isotopes_exact.size();
    isotopes_exact.resize(size_t((isotopes_exact_size - 1) * isotope_distance_ / interpolation_step_ + 1.6)); // round up a bit more

    for (Size i = isotopes_exact_size - 1; i; --i)
    {
      // we don't need to move the 0-th entry
      isotopes_exact[size_t(CoordinateType(i) * isotope_distance_ / interpolation_step_ / charge_ + 0.5)]
        =   isotopes_exact[i];
      isotopes_exact[i] = 0;
    }

    ////
    // compute the Gaussian/Cauchy distribution (to be added for widening the averagine isotope distribution)
    ////
    ContainerType peak_shape_values_y;
    // fill a container with CoordinateType points (x values)
    CoordinateType peak_width = 0.0;
    if (param_.getValue("isotope:mode:mode") == "Gaussian")
    {
      // Actual width for values in the smooth table for normal distribution
      peak_width = isotope_stdev_ * 4.0;  // MAGIC alert, num stdev for smooth table for normal distribution
      ContainerType peak_shape_values_x;
      for (double coord = -peak_width; coord <= peak_width;
           coord += interpolation_step_)
      {
        peak_shape_values_x.push_back(coord);
      }
      // compute normal approximation at these CoordinateType points (y values)
      Math::BasicStatistics<> normal_widening_model;
      normal_widening_model.setSum(1);
      normal_widening_model.setMean(0);
      normal_widening_model.setVariance(isotope_stdev_ * isotope_stdev_);
      normal_widening_model.normalApproximation(peak_shape_values_y, peak_shape_values_x);
    }
    else if (param_.getValue("isotope:mode:mode") == "Lorentzian")
    {
      peak_width = isotope_lorentz_fwhm_ * 8.0; // MAGIC alert: Lorentzian has infinite support, but we need to stop sampling at some point: 8*FWHM
      for (double coord = -peak_width; coord <= peak_width;
           coord += interpolation_step_)
      {
        boost::math::cauchy_distribution<double> cauchy(0., isotope_lorentz_fwhm_ / 2.0);
        double x = boost::math::pdf(cauchy, coord);
        peak_shape_values_y.push_back(x); //cauchy is using HWHM not FWHM
      }
    }

    ///
    // fold the Gaussian/Lorentzian at each averagine peak, i.e. fill linear interpolation
    ///
    const ContainerType & left = isotopes_exact;
    const ContainerType & right = peak_shape_values_y;
    ContainerType & result = interpolation_.getData();
    result.clear();

    SignedSize r_max = std::min(SignedSize(left.size() + right.size() - 1),
                                SignedSize(2 * peak_width / interpolation_step_ * max_isotope_ + 1));
    result.resize(r_max, 0);

    // we loop backwards because then the small products tend to come first
    // (for better numerics)
    for (SignedSize i = left.size() - 1; i >= 0; --i)
    {
      if (left[i] == 0)
        continue;
      for (SignedSize j = std::min(r_max - i, SignedSize(right.size())) - 1; j >= 0; --j)
      {
        result[i + j] += left[i] * right[j];
      }
    }

    monoisotopic_mz_ = mean_ - isotopes_mean;
    interpolation_.setMapping(interpolation_step_, peak_width / interpolation_step_, monoisotopic_mz_);

    //std::cerr << "mono now: " << monoisotopic_mz_ << " mono easy: " << formula.getMonoWeight()/formula.getCharge() << "\n";

    // scale data so that integral over distribution equals one
    // multiply sum by interpolation_step_ -> rectangular approximation of integral
    IntensityType factor = scaling_ / (interpolation_step_ * std::accumulate(result.begin(), result.end(), IntensityType(0)));
    for (ContainerType::iterator iter = result.begin(); iter != result.end(); ++iter)
    {
      *iter *= factor;
    }
  }

  void IsotopeModel::setOffset(CoordinateType offset)
  {
    double diff = offset - getInterpolation().getOffset();
    mean_ += diff;
    monoisotopic_mz_ += diff;

    InterpolationModel::setOffset(offset);

    param_.setValue("statistics:mean", mean_);
  }

  IsotopeModel::CoordinateType IsotopeModel::getOffset()
  {
    return getInterpolation().getOffset();
  }

  UInt IsotopeModel::getCharge()
  {
    return charge_;
  }

  IsotopeModel::CoordinateType IsotopeModel::getCenter() const
  {
    return monoisotopic_mz_;
  }

  void IsotopeModel::updateMembers_()
  {
    InterpolationModel::updateMembers_();

    charge_ = param_.getValue("charge");
    isotope_stdev_ = param_.getValue("isotope:mode:GaussianSD");
    isotope_lorentz_fwhm_ = param_.getValue("isotope:mode:LorentzFWHM");
    mean_ = param_.getValue("statistics:mean");
    max_isotope_ = param_.getValue("isotope:maximum");
    trim_right_cutoff_ = param_.getValue("isotope:trim_right_cutoff");
    isotope_distance_ = param_.getValue("isotope:distance");

    averagine_[C] = param_.getValue("averagines:C");
    averagine_[H] = param_.getValue("averagines:H");
    averagine_[N] = param_.getValue("averagines:N");
    averagine_[O] = param_.getValue("averagines:O");
    averagine_[S] = param_.getValue("averagines:S");

  }

}


#ifndef COSMO_PROFILES
#define COSMO_PROFILES

#include "COSMO_SAC/util.hpp"
#include <Eigen/Dense>
#include <map>
#include <limits>
#include <iterator>
#include <algorithm>
#include <cctype>
#include <functional>
#include "nlohmann/json.hpp"

namespace COSMOSAC {

/**
A single sigma profile.  In this data structure (for consistency with the 2005 Virginia Tech
database of COSMO-SAC parameters), the first column is the electron density in [e/A^2], and the second column
is the probability of finding a segment with this electron density, multiplied by the segment area, in A^2
*/
class SigmaProfile {
public:
    Eigen::ArrayXd m_sigma, m_psigmaA;
    /// Default constructor
    SigmaProfile() {};
    /// Copy-by-reference constructor
    SigmaProfile(const Eigen::ArrayXd &sigma, const Eigen::ArrayXd &psigmaA) : m_sigma(sigma), m_psigmaA(psigmaA) {};
    /// Move constructor
    SigmaProfile(Eigen::ArrayXd &&sigma, Eigen::ArrayXd &&psigmaA) : m_sigma(sigma), m_psigmaA(psigmaA) {};
    const Eigen::ArrayXd &psigmaA() const { return m_psigmaA; }
    const Eigen::ArrayXd &sigma() const { return m_sigma; }
    const Eigen::ArrayXd psigma(double A_i) const { return m_psigmaA/A_i; }

};

struct SigmaProfileSet {
    SigmaProfile nhb, ///< The profile for the non-hydrogen-bonding segments
        oh, ///< The profile for the OH-bonding segments
        ot; ///< The profile for the "other" segments
};

/// The sigma profiles (and some more metadata) associated with the fluid
struct FluidProfiles {
    enum class dispersion_classes{DISP_WATER, DISP_COOH, DISP_NHB, DISP_ONLY_ACCEPTOR, DISP_DONOR_ACCEPTOR};
    SigmaProfileSet profiles;
    std::string name;
    std::size_t VTnumber;
    double A_COSMO_A2; ///< The surface area of the molecule as calculated by COSMO-SAC, in A^2
    double V_COSMO_A3; ///< The volume of the molecule as calculated by COSMO-SAC, in A^3
    dispersion_classes dispersion_flag;
    double dispersion_eoverkB;
    nlohmann::json meta; ///< Any additional metadata, stored in JSON format
};

///
class ProfileDatabase {
private:
    std::map<std::string, FluidProfiles> datadb;
public:
    FluidProfiles get_profile(const std::string & identifier) const{
        if (datadb.find(identifier) != datadb.cend()) {
            return datadb.find(identifier)->second;
        }
        else {
            throw std::invalid_argument("Unable to load the profile for the identifier [" + identifier + "]");
        }
    }
    virtual std::string normalize_identifier(const std::string &identifier) const { return identifier; };
    std::size_t length(const std::string & identifier) {
        return datadb.size();
    }
    void clear() {
        datadb.clear();
    }
    void add_to_db(const std::string & identifier, FluidProfiles &&value) {
        datadb[identifier] = value;
    }
};

struct VTFluidInfo {
    int VTIndex;
    std::string numstring;
    std::string name;
    std::string CAS;
    double V_COSMO_A3;
};
void to_json(nlohmann::json& j, const VTFluidInfo& p) {
    j = nlohmann::json{{"VTIndex", p.VTIndex}, {"numstring", p.numstring}, {"name", p.name}, {"CAS", p.CAS},  {"V_COSMO_A3", p.V_COSMO_A3}};
}
void from_json(const nlohmann::json& j, VTFluidInfo& p) {
    j.at("VTIndex").get_to(p.VTIndex);
    j.at("numstring").get_to(p.numstring);
    j.at("name").get_to(p.name);
    j.at("CAS").get_to(p.CAS);
    j.at("V_COSMO_A3").get_to(p.V_COSMO_A3);
}

/// The format of the VirginiaTech database of profiles
class VirginiaTechProfileDatabase : public ProfileDatabase{
private:

    std::map<std::string, VTFluidInfo> m_VTdata;
    const std::string m_dbpath;
public:
    VirginiaTechProfileDatabase(const std::string &tab_delimited_summary, const std::string &dbpath) : m_dbpath(dbpath) {
        // First, read in the file of summary information
        auto lines = str_split(get_file_contents(tab_delimited_summary), "\n");
        if (lines.size() == 1){
            lines = str_split(get_file_contents(tab_delimited_summary), "\r");
        }
        if (lines.size() == 1){
            throw std::invalid_argument("Unable to match the line endings in file " + tab_delimited_summary);
        }
        // Iterate over the lines, storing an entry for each one
        for (auto &&line : lines) {
            VTFluidInfo fluid;
            if (line.empty()) { continue; }
            auto elements = str_split(strstrip(line), "\t");
            if (elements.size() < 6) {
                throw std::invalid_argument("Need at least 6 entries in line");
            }
            if (elements[0] == "Index No.") {
                continue;
            }
            fluid.VTIndex = mystrtoi(elements[0]);
            // Index 1 is a chemical formula, but non-standard form
            fluid.name = elements[2];
            fluid.CAS = elements[3];
            fluid.V_COSMO_A3 = mystrtod(elements[5]);
            char num[5];
            snprintf(num, sizeof(num), "%04d", static_cast<int>(fluid.VTIndex));
            fluid.numstring = num;
            if (m_VTdata.find(fluid.numstring) != m_VTdata.end()) {
                throw std::invalid_argument("Cannot have duplicate key: "+ fluid.numstring);
            }
            else{
                m_VTdata[fluid.numstring] = fluid;
            }

        }
    }
    std::string normalize_identifier(const std::string &identifier) const override {
        // The key is in the database, return it
        if (m_VTdata.find(identifier) != m_VTdata.end()) {
            return identifier;
        }
        for (auto &el : m_VTdata) {
            auto &fluid = el.second;
            if (fluid.name == identifier)
            {
                // Construct the fluid identifier from the identifier that is provided
                char num[5];
                snprintf(num, sizeof(num), "%04d", static_cast<int>(fluid.VTIndex));
                return num;
            }
            if (fluid.CAS == identifier)
            {
                // Construct the fluid identifier from the identifier that is provided
                char num[5];
                snprintf(num, sizeof(num), "%04d", static_cast<int>(fluid.VTIndex));
                return num;
            }
        }
        throw std::invalid_argument("Unable to match identifier in secondary lookup");
    }
    void add_profile(std::string identifier) {
        auto info = m_VTdata[identifier];
        // Now we load the sigma profile(s) from the file
        auto lines = str_split(get_file_contents(m_dbpath + "/VT2005-" + info.numstring + "-PROF.txt"));
        std::vector<double> sigma, psigmaA;
        FluidProfiles fluid;
        for (auto &&line : lines) {
            if (line.size() > 10) {
                auto _sigma = mystrtod(line.substr(0, 25));
                auto _psigmaA = mystrtod(line.substr(25, 25));
                sigma.push_back(_sigma);
                psigmaA.push_back(_psigmaA);
            }
        }
        fluid.name = info.name;
        fluid.VTnumber = info.VTIndex;
        fluid.V_COSMO_A3 = info.V_COSMO_A3;
        if (sigma.size() == 51 && psigmaA.size() == 51) {
            fluid.profiles.nhb = SigmaProfile(Eigen::Map<Eigen::ArrayXd>(&(sigma[0]), sigma.size()),
                Eigen::Map<Eigen::ArrayXd>(&(psigmaA[0]), psigmaA.size()));
            fluid.A_COSMO_A2 = fluid.profiles.nhb.psigmaA().sum();
        }
        else {
            throw std::invalid_argument("Don't support 2 & 3 sigma profiles yet");
        }
        add_to_db(info.numstring, std::move(fluid));
    }
    std::string to_JSON(){
        auto nspaces = 2;
        nlohmann::json j = m_VTdata;
        return j.dump(nspaces);
    }
};

struct DelawareFluidInfo {
    std::size_t DelIndex;
    std::string CAS;
    std::string formula;
    std::string name;
    std::string SMILES;
    std::string InChIString; ///< Standard InChI string
    std::string InChIKey; ///< Standard InChI key
    double V_COSMO_A3;
    nlohmann::json meta;
};
void to_json(nlohmann::json& j, const DelawareFluidInfo& p) {
    j = nlohmann::json{
        {"DelIndex", p.DelIndex}, {"CAS", p.CAS}, {"formula", p.formula},  {"name", p.name},
        {"SMILES", p.SMILES}, {"InChIString", p.InChIString}, {"InChIKey", p.InChIKey},  {"V_COSMO_A3", p.V_COSMO_A3}
};
}
void from_json(const nlohmann::json& j, DelawareFluidInfo& p) {
    j.at("DelIndex").get_to(p.DelIndex);
    j.at("CAS").get_to(p.CAS);
    j.at("formula").get_to(p.formula);
    j.at("name").get_to(p.name);
    j.at("SMILES").get_to(p.SMILES);
    j.at("InChIString").get_to(p.InChIString);
    j.at("InChIKey").get_to(p.InChIKey);
    j.at("V_COSMO_A3").get_to(p.V_COSMO_A3);
    j.at("name").get_to(p.name);
}

/// The format of the Delaware database of profiles
class DelawareProfileDatabase : public ProfileDatabase {
private:

    std::map<std::string, DelawareFluidInfo> m_Deldata;
    const std::string m_dbpath;
public:
    DelawareProfileDatabase(const std::string &space_delimited_summary, const std::string &dbpath) : m_dbpath(dbpath) {
        // First, read in the file of summary information
        auto lines = str_split(get_file_contents(space_delimited_summary), "\n");
        // Iterate over the lines, storing an entry for each one
        for (auto &&line : lines) {
            DelawareFluidInfo fluid;
            if (line.empty()) { continue; }
            auto elements = str_split(strrstrip(line), " ");
            if (elements[0] == "ID") {
                continue;
            }
            fluid.DelIndex = mystrtoi(elements[0]);
            fluid.formula = elements[1];
            fluid.CAS = elements[2];
            fluid.name = elements[3];
            fluid.SMILES = elements[4];
            fluid.InChIString = elements[5];
            fluid.InChIKey = elements[6];
            m_Deldata[fluid.InChIKey] = fluid;
        }
    }
    std::string normalize_identifier(const std::string &identifier) const override {
        // The key is in the database, return it
        if (m_Deldata.find(identifier) != m_Deldata.end()) {
            return identifier;
        }
        for (auto &el : m_Deldata) {
            auto &fluid = el.second;
            if (fluid.name == identifier)
            {
                return fluid.InChIKey;
            }
            if (fluid.CAS == identifier)
            {
                return fluid.InChIKey;
            }
        }
        throw std::invalid_argument("Unable to match identifier in secondary lookup");
    }
    void add_profile(std::string identifier) {
        auto info = m_Deldata[identifier];
        // Now we load the sigma profile(s) from the file
        auto lines = str_split(get_file_contents(m_dbpath + "/" + info.InChIKey + ".sigma"));

        std::vector<double> _sigma, _psigmaA;
        FluidProfiles fluid;
        std::string meta;
        for (auto &&line : lines) {
            if (line.substr(0,8) == "# Name: "){ std::string check_name = line.substr(8,line.size()-8); continue;}
            if (line.substr(0,8) == "# CASn: ") { std::string check_CAS = line.substr(8, line.size() - 8); continue; }
            if (line.substr(0, 8) == "# meta: ") { meta = line.substr(8, line.size() - 8); continue; }
            if (line[0] == '#'){ continue; }
            auto v = str_split(strrstrip(line), " ");
            if (v.empty() || (v.size() ==1 && v[0].empty())){ continue; }
            _sigma.push_back(mystrtod(v[0]));
            _psigmaA.push_back(mystrtod(v[1]));
        }
        Eigen::Map<Eigen::ArrayXd> sigma(&(_sigma[0]), _sigma.size());
        Eigen::Map<Eigen::ArrayXd> psigmaA(&(_psigmaA[0]), _psigmaA.size());
        fluid.name = info.name;

        fluid.VTnumber = info.DelIndex;
        if (sigma.size() == 51 && psigmaA.size() == 51) {
            fluid.profiles.nhb = SigmaProfile(sigma, psigmaA);
            fluid.A_COSMO_A2 = fluid.profiles.nhb.psigmaA().sum();
        }
        else if (sigma.size() == 51*3 && psigmaA.size() == 51*3){
            fluid.profiles.nhb = SigmaProfile(sigma.segment(0*51, 51), psigmaA.segment(0*51, 51));
            fluid.profiles.oh =  SigmaProfile(sigma.segment(1*51, 51), psigmaA.segment(1*51, 51));
            fluid.profiles.ot =  SigmaProfile(sigma.segment(2*51, 51), psigmaA.segment(2*51, 51));
            double check_Area2 = fluid.profiles.nhb.psigmaA().sum() + fluid.profiles.oh.psigmaA().sum() + fluid.profiles.ot.psigmaA().sum();
            fluid.A_COSMO_A2 = check_Area2;
        }
        else{
            throw std::invalid_argument("Length of sigma profile ["+std::to_string(sigma.size())+"] is neither 51 nor 51*3");
        }
        fluid.meta = nlohmann::json::parse(meta);
        fluid.V_COSMO_A3 = fluid.meta["volume [A^3]"];
        std::string flag = fluid.meta["disp. flag"];
        if (flag == "COOH") {
            fluid.dispersion_flag = FluidProfiles::dispersion_classes::DISP_COOH;
        }
        else if (flag == "H2O") {
            fluid.dispersion_flag = FluidProfiles::dispersion_classes::DISP_WATER;
        }
        else if (flag == "NHB") {
            fluid.dispersion_flag = FluidProfiles::dispersion_classes::DISP_NHB;
        }
        else if (flag == "HB-ACCEPTOR") {
            fluid.dispersion_flag = FluidProfiles::dispersion_classes::DISP_ONLY_ACCEPTOR;
        }
        else if (flag == "HB-DONOR-ACCEPTOR") {
            fluid.dispersion_flag = FluidProfiles::dispersion_classes::DISP_DONOR_ACCEPTOR;
        }
        else {
            throw std::invalid_argument("Unable to match dispersion flag: \""+flag+"\"");
        }
        if (fluid.meta["disp. e/kB [K]"].is_null()){
            // The null from JSON is mapped to a NaN of C++
            fluid.dispersion_eoverkB = std::numeric_limits<double>::quiet_NaN();
        }
        else {
            fluid.dispersion_eoverkB = fluid.meta["disp. e/kB [K]"];
        }
        add_to_db(info.InChIKey, std::move(fluid));
    }
    std::string to_JSON(){
        auto nspaces = 2;
        nlohmann::json j = m_Deldata;
        return j.dump(nspaces);
    }
};

} /* namespace COSMOSAC */

#endif


// Last update: April 5, 2013
// This is main.
// This is MD simulation of an electrolyte confined within planar walls
// Problem : Compute density profiles of ions trapped within planar walls
/* Useful studies :
		     1. Role of dielectric contrast
		     2. Role of valency of ions
		     3. Role of varying salt concentration
// To implement:
		     1. Charged walls

*/
/* lessons learned :
		     1. do not use exit(1) to exit the program. this leads to memory leaks.

*/

#include <boost/program_options.hpp>
#include <time.h>
#include "utility.h"
#include "interface.h"
#include "particle.h"
#include "vertex.h"
#include "BIN.h"
#include "control.h"
#include "functions.h"
#include "precalculations.h"
#include "thermostat.h"
#include "fmd.h"

//MPI boundary parameters
unsigned int lowerBoundIons;
unsigned int upperBoundIons;
unsigned int sizFVecIons;
unsigned int lowerBoundMesh;
unsigned int upperBoundMesh;
unsigned int sizFVecMesh;

mpi::environment env;
mpi::communicator world;

void
cpmd(vector<PARTICLE> &, INTERFACE &, vector<THERMOSTAT> &, vector<THERMOSTAT> &, vector<BIN> &, CONTROL &, CONTROL &);
// removed particle colloid, fake thermostat and fmdremote

int main(int argc, char *argv[]) {

    // Electrostatic system variables
    double bx, by, bz;        // lengths of the box
    double ein;            // permittivity of inside medium
    double e_left;                // permittivity of leftside medium
    double e_right;               // permittivity of rightside medium
    int pz_in;            // positive valency of ions inside
    int nz_in;            // negative valency of ions inside
    double salt_conc_in;        // salt concentration outside	(enter in M)
    double saltion_diameter_in;    // inside salt ion diameter	(positive and negative ions assumed to have same diameter at this point)
    double T, fake_T;            // temperature at which the system of ions is

    // Simulation related variables
    double fraction_diameter;        // fraction that multiplies the diameter to generate the discretization width for the interface
    double Q, fake_Q;                // thermostat mass required to generate canonical ensemble
    unsigned int chain_length_real, chain_length_fake;
    double bin_width;            // width of the bins used to compute density profiles
    CONTROL fmdremote, cpmdremote;            // remote control for cpmd

    // Different parts of the system
    vector<PARTICLE> saltion_in;        // salt ions inside
    vector<PARTICLE> ion;        // all ions in the system
    INTERFACE box;            // interface, z planes hard walls; rest periodic boundaries

    // Analysis
    vector<BIN> bin;            // bins

    // Fake system
    double fakemass;

    // Get input values from the user
    options_description desc("Usage:\nrandom_mesh <options>");
    desc.add_options()
            ("help,h", "print usage message")
            ("bx,X", value<double>(&bx)->default_value(22.848),
             "box length in x direction in nanometers")                    // enter in nanometers
            ("by,Y", value<double>(&by)->default_value(22.848),
             "box length in y direction in nanometers")                    // enter in nanometers
            ("bz,Z", value<double>(&bz)->default_value(3),
             "box length in z direction in nanometers")                    // enter in nanometers
            ("epsilon_in,e", value<double>(&ein)->default_value(80.1), "dielectric const inside")
            ("epsilon_left,l", value<double>(&e_left)->default_value(20), "dielectric const leftside")
            ("epsilon_right,r", value<double>(&e_right)->default_value(80.1), "dielectric const rightside")
            ("pz_in,p", value<int>(&pz_in)->default_value(1), "positive valency inside")
            ("nz_in,n", value<int>(&nz_in)->default_value(-1), "negative valency inside")
            ("salt_conc_in,c", value<double>(&salt_conc_in)->default_value(0.1), "salt concentration inside")
            ("saltion_diameter_in,d", value<double>(&saltion_diameter_in)->default_value(0.714),
             "salt ion diameter inside")        // enter in nanometers
            ("fraction_diameter,g", value<double>(&fraction_diameter)->default_value(1.4),
             "for interface discretization width")
            ("thermostat_mass,Q", value<double>(&Q)->default_value(1.0), "thermostat mass")
            ("chain_length_real,L", value<unsigned int>(&chain_length_real)->default_value(5),
             "chain length for real system: enter L+1 if you want L thermostats")
            ("fake_temperature,k", value<double>(&fake_T)->default_value(0.002), "fake temperature")
            ("fake_thermostat_mass,q", value<double>(&fake_Q)->default_value(1.0), "fake thermostat mass")
            ("chain_length_fake,D", value<unsigned int>(&chain_length_fake)->default_value(5),
             "chain length for fake system: enter L+1 if you want L thermostats")
            ("fakemass,f", value<double>(&fakemass)->default_value(100.0), "fake mass")
            ("cpmd_fake_mass,M", value<double>(&cpmdremote.fakemass)->default_value(1.0), "cpmd fake mass")
            ("bin_width,B", value<double>(&bin_width)->default_value(0.1), "bin width")
            ("cpmd_timestep,T", value<double>(&cpmdremote.timestep)->default_value(0.0005), "time step used in cpmd")
            ("fmd_steps,s", value<int>(&fmdremote.steps)->default_value(200), "steps used in fmd")
            ("cpmd_steps,S", value<int>(&cpmdremote.steps)->default_value(1000), "steps used in cpmd")
            ("cpmd_eqm,P", value<int>(&cpmdremote.hiteqm)->default_value(10), "production begin (cpmd)")
            ("cpmd_freq,F", value<int>(&cpmdremote.freq)->default_value(10), "sample frequency (cpmd)")
            ("cpmd_verify,y", value<int>(&cpmdremote.verify)->default_value(1000), "verify (cpmd)")
            ("cpmd_extra_compute,x", value<int>(&cpmdremote.extra_compute)->default_value(10),
             "compute additional (cpmd)")
            ("cpmd_writedensity,w", value<int>(&cpmdremote.writedensity)->default_value(100), "write density files")
            ("cpmd_movie_freq,m", value<int>(&cpmdremote.moviefreq)->default_value(100), "compute additional (cpmd)");

    variables_map vm;
    store(parse_command_line(argc, argv, desc), vm);
    notify(vm);
    if (vm.count("help")) {
        std::cout << desc << "\n";
        return 0;
    }

    // Set up the system
    T = 1;        // set temperature
    box.ein = ein;
    box.e_left = e_left;
    box.e_right = e_right;
    box.set_up(salt_conc_in, 0, 0, pz_in, 0, 0, bx / unitlength, by / unitlength, bz / unitlength);
    box.put_saltions_inside(saltion_in, pz_in, nz_in, salt_conc_in, saltion_diameter_in, ion);
    // box.put_one_saltion_in_center(saltion_in, pz_in, nz_in, saltion_diameter_in, ion);						//put one ion in center

    make_bins(bin, box, bin_width);    // set up bins to be used for computing density profiles
    vector<double> initial_density;
    bin_ions(ion, box, initial_density, bin);    // bin the ions to get initial density profile

//   box.number_of_vertices = total_gridpoints;
    box.discretize(saltion_diameter_in / unitlength, fraction_diameter);

    fmdremote.fakemass = fakemass;

    // output to screen the parameters of the problem
    if (world.rank() == 0) {
        cout << "\nProgram starts\n";
        cout << "Dielectric constant of water is " << epsilon_water << endl;
        cout << "Reduced units: scalefactor entering in Coloumb interaction is " << scalefactor << endl;
        cout << "Box dimensions x | y | z " << setw(15) << box.lx << setw(15) << box.ly << setw(15) << box.lz << endl;
        cout << "Permittivity inside " << box.ein << endl;
        cout << "Permittivity leftside " << box.e_left << endl;
        cout << "Permittivity rightside " << box.e_right << endl;
        cout << "Contrast strength left " << 2 * (box.e_left - box.ein) / (box.e_left + box.ein) << endl;
        cout << "Contrast strength right " << 2 * (box.e_right - box.ein) / (box.e_right + box.ein) << endl;
        cout << "Positive ion valency inside " << pz_in << endl;
        cout << "Negative ion valency inside " << nz_in << endl;
        cout << "Salt ion diameter inside " << saltion_diameter_in / unitlength << endl;
        cout << "Salt concentration inside " << salt_conc_in << endl;
        cout << "Debye length inside " << box.inv_kappa_in << endl;
        cout << "Mean separation inside " << box.mean_sep_in << endl;
        cout << "Number of salt ions inside " << saltion_in.size() << endl;
        cout << "Temperature " << T << endl;
        cout << "Binning width (uniform) " << bin[0].width << endl;
        cout << "Number of bins " << bin.size() << endl;
        cout << "Number of points discretizing the left and right z planes " << box.leftplane.size() << setw(10)
             << box.rightplane.size() << endl;
    }

    int numOfNodes = world.size();
    if (world.rank() == 0) {
#pragma omp parallel default(shared)
        {
            if (omp_get_thread_num() == 0) {
                printf("The app comes with MPI and OpenMP (Hybrid) parallelization)\n");
                printf("Number of MPI processes used %d\n", numOfNodes);
                printf("Number of OpenMP threads per MPI process %d\n", omp_get_num_threads());
                printf("Make sure that number of grid points / ions is greater than %d\n",
                       omp_get_num_threads() * numOfNodes);
            }
        }
    }

    // write to files

    // initial configuration
    if (world.rank() == 0) {
        ofstream initial_configuration("outfiles/initialconfig.dat");
        for (unsigned int i = 0; i < ion.size(); i++)
            initial_configuration << "ion" << setw(5) << ion[i].id << setw(15) << "charge" << setw(5) << ion[i].q
                                  << setw(15) << "position" << setw(15) << ion[i].posvec << endl;
        initial_configuration.close();

        // initial density
        ofstream density_profile("outfiles/initial_density_profile.dat", ios::out);
        for (unsigned int b = 0; b < initial_density.size(); b++)
            density_profile << bin[b].lower << setw(15) << initial_density.at(b) << endl;
        density_profile.close();
    }
    // check point
    double totalions = 0;
    for (unsigned int b = 0; b < initial_density.size(); b++)
        totalions += initial_density.at(b) * bin[b].volume;
    if (world.rank() == 0)
        cout << "total ions " << totalions << "  should be " << ion.size() << endl;
    int totalpions = 0, totalnions = 0;
    for (unsigned int i = 0; i < ion.size(); i++) {
        if (ion[i].valency > 0)
            totalpions += 1;
        else if (ion[i].valency < 0)
            totalnions += 1;
    }

    // some calculations before simulation begins
    double charge = box.total_charge_inside(ion);

    if (world.rank() == 0) {
        cout << "Total positive ions " << totalpions << endl;
        cout << "Total negative ions " << totalnions << endl;
        cout << "Total charge inside" << charge << endl;
    }
    // prepare for cpmd : make real baths
    vector<THERMOSTAT> real_bath;
    if (chain_length_real == 1)
        real_bath.push_back(THERMOSTAT(0, T, 3 * ion.size(), 0.0, 0, 0));
    else {
        real_bath.push_back(THERMOSTAT(Q, T, 3 * ion.size(), 0, 0, 0));
        while (real_bath.size() != chain_length_real - 1)
            real_bath.push_back(THERMOSTAT(Q / (3 * ion.size()), T, 1, 0, 0, 0));
        real_bath.push_back(THERMOSTAT(0, T, 3 * ion.size(), 0.0, 0, 0));
        // final bath is dummy bath (dummy bath always has zero mass)
    }

    vector<THERMOSTAT> fake_bath;
    if (chain_length_fake == 1)
        fake_bath.push_back(THERMOSTAT(0, fake_T, box.leftplane.size(), 0.0, 0, 0));
    else {
        fake_bath.push_back(THERMOSTAT(fake_Q, fake_T, box.leftplane.size(), 0, 0, 0));
        while (fake_bath.size() != chain_length_fake - 1)
            fake_bath.push_back(THERMOSTAT(fake_Q / box.leftplane.size(), fake_T, 1, 0, 0, 0));
        fake_bath.push_back(THERMOSTAT(0, fake_T, box.leftplane.size(), 0.0, 0,
                                       0));            // finally, the coding trick: dummy bath (dummy bath always has zero mass)
    }

    // Induced charge system
    // fake positions initialized
    for (unsigned int i = 0; i < box.leftplane.size(); i++)
        box.leftplane[i].w = 0.0;
    double total_ind_charge_left = 0;
    for (unsigned int i = 0; i < box.leftplane.size(); i++)
        total_ind_charge_left += box.leftplane[i].w * box.leftplane[i].a;

    for (unsigned int i = 0; i < box.rightplane.size(); i++)
        box.rightplane[i].w = 0.0;
    double total_ind_charge_right = 0;
    for (unsigned int i = 0; i < box.rightplane.size(); i++)
        total_ind_charge_right += box.rightplane[i].w * box.rightplane[i].a;

    if (world.rank() == 0) {
        cout << "total induced left interface" << total_ind_charge_left << endl;
        cout << "total induced right interface" << total_ind_charge_right << endl;
    }

    clock_t t;
    t = clock();
    precalculate(box); // calculate the Greens function within interface or between interface
    t = clock() - t;
    //exact incuded charge density calculation
    //vector<VERTEX> exact_induced_density_left;
    //vector<VERTEX> exact_induced_density_right;

    //exact_induced_density_left = box.compute_exact_induced_density(ion,'l');
    //exact_induced_density_right = box.compute_exact_induced_density(ion,'r');
    //double total_energy = box.exact_total_energy(exact_induced_density_left, exact_induced_density_right, ion);
    //cout << "total energy should be " << total_energy << endl;
    if (world.rank() == 0)
        cout << "pre_calculation finished,  time taken: " << ((double) t) / CLOCKS_PER_SEC << " seconds" << endl;

    // Fictitious Molecular Dynamics

    t = clock();
    fmdremote.timestep = 0.0005;
    fmdremote.verify = 0;
    fmdremote.anneal = 'y';
    fmd(ion, box, fmdremote, cpmdremote);
    t = clock() - t;
    if (world.rank() == 0)
        cout << "fmd calculation finished,  time taken: " << ((double) t) / CLOCKS_PER_SEC << " seconds" << endl;
/*
  ofstream induced_density_left ("outfiles_windows/induced_density_left.dat");
  for (unsigned int k = 0; k < box.leftplane.size(); k++)
    induced_density_left << k+1 <<setw(15) << box.leftplane[k].posvec << setw(15) <<
    sqrt(box.leftplane[k].posvec.x * box.leftplane[k].posvec.x + box.leftplane[k].posvec.y * box.leftplane[k].posvec.y) << setw(15) <<
    box.leftplane[k].w << setw(15) << box.leftplane[k].wmean << setw(15)<< exact_induced_density_left[k].w << endl;		// NOTE plotting also the cartesian coordinates
  induced_density_left.close();
  ofstream induced_density_right("outfiles_windows/induced_density_right.dat");
  for (unsigned int k = 0; k < box.rightplane.size(); k++)
    induced_density_right << k+1 <<setw(15) << box.rightplane[k].posvec << setw(15) <<
    sqrt(box.rightplane[k].posvec.x * box.rightplane[k].posvec.x + box.rightplane[k].posvec.y * box.rightplane[k].posvec.y) << setw(15) <<
    box.rightplane[k].w << setw(15) << box.rightplane[k].wmean << setw(15)<< exact_induced_density_right[k].w << endl;		// NOTE plotting also the cartesian coordinates
  induced_density_right.close();
 */

    //MPI Boundary calculations for ions
    unsigned int rangeIons = (ion.size() + world.size() - 1) / (1.0 * world.size());
    lowerBoundIons = world.rank() * rangeIons;
    upperBoundIons = (world.rank() + 1) * rangeIons - 1;
    sizFVecIons = rangeIons;
    if (world.rank() == world.size() - 1) {
        upperBoundIons = ion.size() - 1;
    }

    //MPI Boundary calculations for meshPoints
    unsigned int rangeMesh = (box.leftplane.size() + world.size() - 1) / (1.0 * world.size());
    lowerBoundMesh = world.rank() * rangeMesh;
    upperBoundMesh = (world.rank() + 1) * rangeMesh - 1;
    sizFVecMesh = rangeMesh;
    if (world.rank() == world.size() - 1) {
        upperBoundMesh = box.leftplane.size() - 1;
    }

    // Car-Parrinello Molecular Dynamics
    cpmd(ion, box, real_bath, fake_bath, bin, fmdremote, cpmdremote);

//   // Post simulation analysis (useful for short runs, but performed otherwise too)
    //   auto_correlation_function();
    if (world.rank() == 0) {
        cout << "MD trust factor R (should be < 0.05) is " << compute_MD_trust_factor_R(cpmdremote.hiteqm) << endl;
        cout << "Program ends \n\n";
    }

    return 0;
}
// End of main

// useful codes

/* random number generation code from gsl
int N = 10;  // number of random numbers to generate

// random nmbr declarations

const gsl_rng_type * T;
gsl_rng * r;

T = gsl_rng_default;
r = gsl_rng_alloc (T);

// prepare for seeding of gsl random nmbr generator:

//   srand((time(0)));                // srand & time are built-in
//   unsigned long int s = random();  // gsl_rng_uniform will eventually
unsigned long int s = 23897897;  // gsl_rng_uniform will eventually
                                 // want a non-negative "long" integer

gsl_rng_env_setup();
gsl_rng_set(r,s); // seed the random number generator;

// generate N random numbers:

for(int i = 0; i < N; ++i) {
  cout << " random number = " << gsl_rng_uniform (r) << endl;;
  }

gsl_rng_free (r);

return 0;
*/


// Copyright 2008 John Maddock
//
// Use, modification and distribution are subject to 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)

#ifndef BOOST_MATH_DISTRIBUTIONS_DETAIL_HG_QUANTILE_HPP
#define BOOST_MATH_DISTRIBUTIONS_DETAIL_HG_QUANTILE_HPP

#include <boost/math/policies/error_handling.hpp>
#include <boost/math/distributions/detail/hypergeometric_pdf.hpp>

namespace boost{ namespace math{ namespace detail{

template <class T>
inline unsigned round_x_from_p(unsigned x, T p, T cum, T fudge_factor, unsigned lbound, unsigned /*ubound*/, const policies::discrete_quantile<policies::integer_round_down>&)
{
   if((p < cum * fudge_factor) && (x != lbound))
   {
      BOOST_MATH_INSTRUMENT_VARIABLE(x-1);
      return --x;
   }
   return x;
}

template <class T>
inline unsigned round_x_from_p(unsigned x, T p, T cum, T fudge_factor, unsigned /*lbound*/, unsigned ubound, const policies::discrete_quantile<policies::integer_round_up>&)
{
   if((cum < p * fudge_factor) && (x != ubound))
   {
      BOOST_MATH_INSTRUMENT_VARIABLE(x+1);
      return ++x;
   }
   return x;
}

template <class T>
inline unsigned round_x_from_p(unsigned x, T p, T cum, T fudge_factor, unsigned lbound, unsigned ubound, const policies::discrete_quantile<policies::integer_round_inwards>&)
{
   if(p >= 0.5)
      return round_x_from_p(x, p, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_down>());
   return round_x_from_p(x, p, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_up>());
}

template <class T>
inline unsigned round_x_from_p(unsigned x, T p, T cum, T fudge_factor, unsigned lbound, unsigned ubound, const policies::discrete_quantile<policies::integer_round_outwards>&)
{
   if(p >= 0.5)
      return round_x_from_p(x, p, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_up>());
   return round_x_from_p(x, p, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_down>());
}

template <class T>
inline unsigned round_x_from_p(unsigned x, T /*p*/, T /*cum*/, T /*fudge_factor*/, unsigned /*lbound*/, unsigned /*ubound*/, const policies::discrete_quantile<policies::integer_round_nearest>&)
{
   return x;
}

template <class T>
inline unsigned round_x_from_q(unsigned x, T q, T cum, T fudge_factor, unsigned lbound, unsigned /*ubound*/, const policies::discrete_quantile<policies::integer_round_down>&)
{
   if((q * fudge_factor > cum) && (x != lbound))
   {
      BOOST_MATH_INSTRUMENT_VARIABLE(x-1);
      return --x;
   }
   return x;
}

template <class T>
inline unsigned round_x_from_q(unsigned x, T q, T cum, T fudge_factor, unsigned /*lbound*/, unsigned ubound, const policies::discrete_quantile<policies::integer_round_up>&)
{
   if((q < cum * fudge_factor) && (x != ubound))
   {
      BOOST_MATH_INSTRUMENT_VARIABLE(x+1);
      return ++x;
   }
   return x;
}

template <class T>
inline unsigned round_x_from_q(unsigned x, T q, T cum, T fudge_factor, unsigned lbound, unsigned ubound, const policies::discrete_quantile<policies::integer_round_inwards>&)
{
   if(q < 0.5)
      return round_x_from_q(x, q, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_down>());
   return round_x_from_q(x, q, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_up>());
}

template <class T>
inline unsigned round_x_from_q(unsigned x, T q, T cum, T fudge_factor, unsigned lbound, unsigned ubound, const policies::discrete_quantile<policies::integer_round_outwards>&)
{
   if(q >= 0.5)
      return round_x_from_q(x, q, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_down>());
   return round_x_from_q(x, q, cum, fudge_factor, lbound, ubound, policies::discrete_quantile<policies::integer_round_up>());
}

template <class T>
inline unsigned round_x_from_q(unsigned x, T /*q*/, T /*cum*/, T /*fudge_factor*/, unsigned /*lbound*/, unsigned /*ubound*/, const policies::discrete_quantile<policies::integer_round_nearest>&)
{
   return x;
}

template <class T, class Policy>
unsigned hypergeometric_quantile_imp(T p, T q, unsigned r, unsigned n, unsigned N, const Policy& pol)
{
#ifdef BOOST_MSVC
#  pragma warning(push)
#  pragma warning(disable:4267)
#endif
   typedef typename Policy::discrete_quantile_type discrete_quantile_type;
   BOOST_MATH_STD_USING
   BOOST_FPU_EXCEPTION_GUARD
   T result;
   T fudge_factor = 1 + tools::epsilon<T>() * ((N <= boost::math::prime(boost::math::max_prime - 1)) ? 50 : 2 * N);
   unsigned base = static_cast<unsigned>((std::max)(0, (int)(n + r) - (int)(N)));
   unsigned lim = (std::min)(r, n);

   BOOST_MATH_INSTRUMENT_VARIABLE(p);
   BOOST_MATH_INSTRUMENT_VARIABLE(q);
   BOOST_MATH_INSTRUMENT_VARIABLE(r);
   BOOST_MATH_INSTRUMENT_VARIABLE(n);
   BOOST_MATH_INSTRUMENT_VARIABLE(N);
   BOOST_MATH_INSTRUMENT_VARIABLE(fudge_factor);
   BOOST_MATH_INSTRUMENT_VARIABLE(base);
   BOOST_MATH_INSTRUMENT_VARIABLE(lim);

   if(p <= 0.5)
   {
      unsigned x = base;
      result = hypergeometric_pdf<T>(x, r, n, N, pol);
      T diff = result;
      while(result < p)
      {
         diff = (diff > tools::min_value<T>() * 8)
            ? T(n - x) * T(r - x) * diff / (T(x + 1) * T(N + x + 1 - n - r))
            : hypergeometric_pdf<T>(x + 1, r, n, N, pol);
         if(result + diff / 2 > p)
            break;
         ++x;
         result += diff;
#ifdef BOOST_MATH_INSTRUMENT
         if(diff != 0)
         {
            BOOST_MATH_INSTRUMENT_VARIABLE(x);
            BOOST_MATH_INSTRUMENT_VARIABLE(diff);
            BOOST_MATH_INSTRUMENT_VARIABLE(result);
         }
#endif
      }
      return round_x_from_p(x, p, result, fudge_factor, base, lim, discrete_quantile_type());
   }
   else
   {
      unsigned x = lim;
      result = 0;
      T diff = hypergeometric_pdf<T>(x, r, n, N, pol);
      while(result + diff / 2 < q)
      {
         result += diff;
         diff = (diff > tools::min_value<T>() * 8)
            ? x * T(N + x - n - r) * diff / (T(1 + n - x) * T(1 + r - x))
            : hypergeometric_pdf<T>(x - 1, r, n, N, pol);
         --x;
#ifdef BOOST_MATH_INSTRUMENT
         if(diff != 0)
         {
            BOOST_MATH_INSTRUMENT_VARIABLE(x);
            BOOST_MATH_INSTRUMENT_VARIABLE(diff);
            BOOST_MATH_INSTRUMENT_VARIABLE(result);
         }
#endif
      }
      return round_x_from_q(x, q, result, fudge_factor, base, lim, discrete_quantile_type());
   }
#ifdef BOOST_MSVC
#  pragma warning(pop)
#endif
}

template <class T, class Policy>
inline unsigned hypergeometric_quantile(T p, T q, unsigned r, unsigned n, unsigned N, const Policy&)
{
   BOOST_FPU_EXCEPTION_GUARD
   typedef typename tools::promote_args<T>::type result_type;
   typedef typename policies::evaluation<result_type, Policy>::type value_type;
   typedef typename policies::normalise<
      Policy,
      policies::promote_float<false>,
      policies::promote_double<false>,
      policies::assert_undefined<> >::type forwarding_policy;

   return detail::hypergeometric_quantile_imp<value_type>(p, q, r, n, N, forwarding_policy());
}

}}} // namespaces

#endif