#include <iostream>
#include <chrono>
#include <random>
#include <algorithm>
#include <string>
#include <filesystem>
#if defined(_OPENMP)
#include <omp.h>
#endif

class InputParser
{
public:
  InputParser(int &argc, char **argv)
  {
    for (int i = 1; i < argc; ++i)
    {
      this->tokens.push_back(std::string(argv[i]));
    }
  }

  const std::string &getCmdOption(const std::string &option) const
  {
    std::vector<std::string>::const_iterator itr;
    itr = std::find(this->tokens.begin(), this->tokens.end(), option);
    if (itr != this->tokens.end() && ++itr != this->tokens.end())
    {
      return *itr;
    }
    static const std::string empty_string("");
    return empty_string;
  }

  bool cmdOptionExists(const std::string &option) const
  {
    return std::find(this->tokens.begin(), this->tokens.end(), option) != this->tokens.end();
  }

private:
  std::vector<std::string> tokens;
};

struct MonteCarloResult
{
  int64_t tries = 0;
  int64_t count = 0;
  double durationMilli = 0;
};

const int defaultIterationsPerChunk = 1000;

MonteCarloResult computePI(int milliseconds, int iterationsPerChunk)
{
  std::chrono::duration<double, std::milli> diff;
  auto start = std::chrono::system_clock::now();

  int64_t tries = 0;
  int64_t count = 0;

#if defined(_OPENMP)
#pragma omp parallel reduction(+ : count, tries)
#endif
  {
#if defined(_OPENMP)
    std::mt19937 rng(static_cast<unsigned>(omp_get_thread_num()));
#else
    std::mt19937 rng(0);
#endif
    std::uniform_real_distribution<double> dist(0., 1.);

    do
    {
#if defined(_OPENMP)
#pragma omp for nowait
#endif
      for (int64_t i = 0; i < iterationsPerChunk; ++i)
      {
        double x = dist(rng);
        double y = dist(rng);
        if ((x * x + y * y) < 1)
        {
          count++;
        }
        tries++;
      }

#if defined(_OPENMP)
#pragma omp master
#endif
      {
        auto end = std::chrono::system_clock::now();
        diff = end - start;
      }

#if defined(_OPENMP)
#pragma omp barrier
#endif
    } while (diff.count() < milliseconds);
  }

  auto end = std::chrono::system_clock::now();
  diff = end - start;
  MonteCarloResult result{tries, count, diff.count()};
  return result;
}

int main(int argc, char *argv[])
{
  // parse parameters
  InputParser input(argc, argv);
  std::filesystem::path path(argv[0]);

  // help
  if (input.cmdOptionExists("-h") || argc <= 1)
  {
    std::cout << "Usage: " << path.filename() << " -d <milliseconds>" << std::endl;
    std::cout << "  Options:" << std::endl;
    std::cout << "    -d <milliseconds>: Number of milliseconds to compute pi for" << std::endl;
    std::cout << "    -i <iterations>: Number of iterations to compute in one go" << std::endl;
    return 0;
  }

  // error if -d parameter not set
  if (!input.cmdOptionExists("-d"))
  {
    std::cout << "Error: Option -d must be set" << std::endl;
    return -1;
  }

  // check if -i parameter is set and set iterations
  int iterationsPerChunk = defaultIterationsPerChunk;
  if (input.cmdOptionExists("-i"))
  {
    iterationsPerChunk = std::stoi(input.getCmdOption("-i"));
  }

  // get duration
  const int duration = std::stoi(input.getCmdOption("-d"));

  // run Monte Carlo
  MonteCarloResult result = computePI(duration, iterationsPerChunk);

  // format result
  std::cout << "{\"tries\":" << result.tries << ", \"count\":" << result.count << ", \"durationMilli\":" << result.durationMilli << "}" << std::endl;
  return 0;
}
