#include "sme/logger.hpp"
#include "sme/model.hpp"
#include "sme/simulate.hpp"
#include "sme/version.hpp"
#include <QElapsedTimer>
#include <QFile>
#include <fmt/core.h>
#include <limits>

using namespace sme;

static std::string toString(const simulate::SimulatorType &s) {
  if (s == simulate::SimulatorType::DUNE) {
    return "DUNE";
  } else if (s == simulate::SimulatorType::Pixel) {
    return "Pixel";
  }
  return {};
}

struct BenchmarkParams {
  int seconds_per_benchmark{10};
  std::vector<const char *> models{"single-compartment-diffusion",
                                   "ABtoC",
                                   "very-simple-model",
                                   "brusselator-model",
                                   "circadian-clock",
                                   "gray-scott",
                                   "liver-simplified"};
  std::vector<simulate::SimulatorType> simulators{
      simulate::SimulatorType::DUNE, simulate::SimulatorType::Pixel};
  double simulator_timestep{1e-4};
};

static void printHelpMessage() {
  BenchmarkParams params;
  fmt::print("\nUsage:\n");
  fmt::print("\n./benchmark [seconds_per_benchmark=10] [model=all] "
             "[simulator=all] [timestep=1e-3]\n");
  fmt::print("\nPossible values for model:\n");
  for (const auto &model : params.models) {
    fmt::print("  - {}\n", model);
  }
  fmt::print("  - all: all of the above\n");
  fmt::print("\nPossible values for simulator:\n");
  for (const auto &simulator : params.simulators) {
    fmt::print("  - {}\n", toString(simulator));
  }
  fmt::print("  - all: all of the above\n");
}

static BenchmarkParams parseArgs(int argc, char *argv[]) {
  BenchmarkParams params;
  if (argc < 2) {
    return params;
  }
  if (std::string a = argv[1]; (a == "-h") || (a == "--help")) {
    printHelpMessage();
    exit(0);
  } else {
    params.seconds_per_benchmark = std::stoi(argv[1]);
  }
  if (argc > 2) {
    // models
    if (std::string arg = argv[2]; arg != "all") {
      if (auto iter = std::find_if(
              cbegin(params.models), cend(params.models),
              [&arg](const std::string &s) { return s[0] == arg[0]; });
          iter != cend(params.models)) {
        params.models = {*iter};
      } else {
        fmt::print("\nERROR: model '{}' not found\n", arg);
        printHelpMessage();
        exit(1);
      }
    }
  }
  if (argc > 3) {
    // simulators
    if (std::string a = argv[3]; a[0] == 'p' || a[0] == 'P') {
      params.simulators = {simulate::SimulatorType::Pixel};
    } else if (a[0] == 'd' || a[0] == 'D') {
      params.simulators = {simulate::SimulatorType::DUNE};
    }
  }
  if (argc > 4) {
    params.simulator_timestep = std::stod(argv[4]);
  }
  fmt::print("\n# Benchmark parameters:\n");
  fmt::print("# seconds_per_benchmark: {}s\n", params.seconds_per_benchmark);
  fmt::print("# models:\n");
  for (const auto &model : params.models) {
    fmt::print("#   - {}\n", model);
  }
  fmt::print("# simulators:\n");
  for (auto simulator : params.simulators) {
    fmt::print("#   - {}\n", toString(simulator));
  }
  fmt::print("# timestep: {}s\n", params.simulator_timestep);
  return params;
}

static void printSimulatorBenchmarks(const BenchmarkParams &params) {
  // resources contain example models
  Q_INIT_RESOURCE(resources);
  // disable logging
  spdlog::set_level(spdlog::level::off);

  // fixed step-size simulations
  double dt = params.simulator_timestep;
  simulate::Options options;
  options.pixel.maxTimestep = dt;
  options.pixel.maxErr = {std::numeric_limits<double>::max(),
                          std::numeric_limits<double>::max()};
  options.dune.dt = dt;
  options.dune.decrease = 0.5;
  options.dune.increase = 1.5;
  options.dune.maxDt = dt;
  options.dune.minDt = dt * 1e-5;

  for (auto simulator : params.simulators) {
    fmt::print("\n# {} simulator\n", toString(simulator));
    fmt::print("# ms/timestep\ttimesteps\tmodel\n");
    for (const auto &model : params.models) {
      // import model
      model::Model s;
      QFile f(QString(":/models/%1.xml").arg(model));
      f.open(QIODevice::ReadOnly);
      s.importSBMLString(f.readAll().toStdString());
      s.getSimulationSettings().options = options;
      s.getSimulationSettings().simulatorType = simulator;

      // setup simulator
      simulate::Simulation sim(s);
      // do a series of simulations
      // increase length of run by a factor of 2 each time
      // stop when a run takes more than half of runtime_seconds_per_model
      // and use this last run for the benchmark
      QElapsedTimer time;
      int iter{1};
      int ln2iter{0};
      long long elapsed_ms{0};
      while (elapsed_ms < params.seconds_per_benchmark * 500) {
        iter += iter;
        ++ln2iter;
        time.start();
        sim.doMultipleTimesteps({{iter, dt}});
        if (!sim.errorMessage().empty()) {
          fmt::print("Simulation error: {}\n", sim.errorMessage());
          exit(1);
        }
        elapsed_ms = time.elapsed();
      }
      double ms{static_cast<double>(elapsed_ms) / static_cast<double>(iter)};
      fmt::print("{:11.5f}\t2^{:<4}\t\t{}\n", ms, ln2iter, model);
    }
  }
}

int main(int argc, char *argv[]) {
  fmt::print("# Spatial Model Editor v{}\n",
             common::SPATIAL_MODEL_EDITOR_VERSION);
  fmt::print("# Simulator benchmark code\n");
  auto benchmarkParams = parseArgs(argc, argv);
  printSimulatorBenchmarks(benchmarkParams);
}
