// ****************************************************************************
//
//          Aevol - An in silico experimental evolution platform
//
// ****************************************************************************
//
// Copyright: See the AUTHORS file provided with the package or <www.aevol.fr>
// Web: http://www.aevol.fr/
// E-mail: See <http://www.aevol.fr/contact/>
// Original Authors : Guillaume Beslon, Carole Knibbe, David Parsons
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// ****************************************************************************

#include <getopt.h>

#include <cerrno>
#include <cinttypes>
#include <csignal>
#include <cstdlib>

#include <charconv>
#include <filesystem>
#include <format>
#include <string>

#ifdef _OPENMP
#include <omp.h>
#else
typedef int omp_int_t;
inline omp_int_t omp_get_thread_num() { return 0;}
inline omp_int_t omp_get_max_threads() { return 1;}
#endif

#include "aevol.h"


// Helper functions
void print_help(std::filesystem::path prog_path);
auto interpret_cmd_line_args(int argc, char* argv[])
    -> std::tuple<aevol::time_type, aevol::time_type, std::filesystem::path, aevol::time_type, aevol::time_type, bool>;

#ifdef _OPENMP
static bool run_in_parallel = false;
#endif


int main(int argc, char* argv[]) {
  const auto [t0, t_end, ui_output_dir, ui_output_frequency, cli_output_frequency, verbose] =
      interpret_cmd_line_args(argc, argv);

  // Print the hash and date of the commit used to compile this executable
  std::cout << std::format("Running aevol version {}\n", aevol::version_string);

  // =================================================================
  //                          Load the simulation
  // =================================================================
  auto evolution_runner = aevol::EvolutionRunner::make_from_checkpoint(t0);
  evolution_runner->setup_ui_output(ui_output_dir, ui_output_frequency);
  evolution_runner->setup_cli_output(cli_output_frequency);

  // =================================================================
  //                         Run the simulation
  // =================================================================
  evolution_runner->run_evolution(t_end, aevol::exp_setup->selection_pressure());

  return EXIT_SUCCESS;
}

void print_help(std::filesystem::path prog_path) {
  auto prog_name = prog_path.filename().string();

  std::cout << "******************************************************************************\n"
            << "*                                                                            *\n"
            << "*                        aevol - Artificial Evolution                        *\n"
            << "*                                                                            *\n"
            << "* Aevol is a simulation platform that allows one to let populations of       *\n"
            << "* digital organisms evolve in different conditions and study experimentally  *\n"
            << "* the mechanisms responsible for the structuration of the genome and the     *\n"
            << "* transcriptome.                                                             *\n"
            << "*                                                                            *\n"
            << "******************************************************************************\n"
            << "\n"
            << std::format("{}: run an aevol simulation.\n", prog_name)
            << "\n"
            << std::format("Usage : {} -h or --help\n", prog_name)
            << std::format("   or : {} -V or --version\n", prog_name)
            << std::format("   or : {} [-b TIMESTEP] [-e TIMESTEP] [-p NB_THREADS] [-v]\n", prog_name)
            << "\nOptions\n"
            << "  -h, --help\n\tprint this help, then exit\n"
            << "  -V, --version\n\tprint version number, then exit\n"
            << "  -b, --begin TIMESTEP\n"
            << "\tspecify time t0 to resume simulation at (default read in last_gener.txt)\n"
            << "  -e, --end TIMESTEP\n"
            << "\tspecify time of the end of the simulation\n"
            << "\t(if omitted, run for 1000 timesteps)\n"
            << "  -p, --parallel NB_THREADS\n"
            << "\trun on NB_THREADS threads (use -1 for system default)\n"
            << "  -v, --verbose\n\tbe verbose\n"
            << "  --ui-output-dir UI_OUTDIR\n\tdirectory in which to output data for the UI\n"
            << "  --ui-output-frequency NB_GENER\n\tfrequency at which to output data for the UI\n"
            << "  --cli-output-frequency NB_GENER\n\tfrequency at which to output data in the CLI\n";
}

auto interpret_cmd_line_args(int argc, char* argv[])
    -> std::tuple<aevol::time_type, aevol::time_type, std::filesystem::path, aevol::time_type, aevol::time_type, bool> {
  // Command-line option variables
  auto t0     = aevol::time_type{-1};
  auto t_end  = aevol::time_type{-1};
  auto ui_output_dir = std::filesystem::path{"ui_output_dir"};
  auto ui_output_frequency = aevol::time_type{0};
  auto cli_output_frequency = aevol::time_type{100};
  auto verbose = bool{false};

  // Define allowed options
  const char* options_list = "hVb:e:vp:";
  int option_index = 0;
  enum long_opt_codes {
    UI_OUTPUT_DIR = 1000,
    UI_OUTPUT_FREQUENCY,
    CLI_OUTPUT_FREQUENCY
  };
  static struct option long_options_list[] = {
      {"help",          no_argument,       nullptr, 'h'},
      {"version",       no_argument,       nullptr, 'V'},
      {"begin",         required_argument, nullptr, 'b'},
      {"end",           required_argument, nullptr, 'e'},
      {"verbose",       no_argument,       nullptr, 'v'},
      {"parallel",      required_argument, nullptr, 'p'},
      // long-only options
      {"ui-output-dir",         required_argument, nullptr, UI_OUTPUT_DIR},
      {"ui-output-frequency",   required_argument, nullptr, UI_OUTPUT_FREQUENCY},
      {"cli-output-frequency",  required_argument, nullptr, CLI_OUTPUT_FREQUENCY},
      {0, 0, 0, 0}
  };

  // Get actual values of the CLI options
  int option;
  while ((option = getopt_long(argc, argv, options_list, long_options_list, &option_index)) != -1) {
    switch (option) {
      case 'h' : {
        print_help(argv[0]);
        exit(EXIT_SUCCESS);
      }
      case 'V' : {
        aevol::print_aevol_version();
        exit(EXIT_SUCCESS);
      }
      case 'b' : {
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), t0);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg(std::string("invalid value for option ") + long_options_list[option_index].name);
        }
        break;
      }
      case 'e' : {
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), t_end);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg(std::string("invalid value for option ") + long_options_list[option_index].name);
        }
        break;
      }
      case 'v' : {
        verbose = true;
        break;
      }
      case 'p' : {
        #ifdef _OPENMP
        run_in_parallel = true;
        auto num_threads = int{};
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), num_threads);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg(std::string("invalid value for option ") + long_options_list[option_index].name);
        }
        if (num_threads > 0) {
          omp_set_num_threads(num_threads);
        }
        #endif
        break;
      }
      // Handle long-only options
      case UI_OUTPUT_DIR: {
        ui_output_dir = optarg;
        ui_output_frequency = 100;
        break;
      }
      case UI_OUTPUT_FREQUENCY: {
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), ui_output_frequency);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg(std::string("invalid value for option ") + long_options_list[option_index].name);
        }
        break;
      }
      case CLI_OUTPUT_FREQUENCY: {
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), cli_output_frequency);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg(std::string("invalid value for option ") + long_options_list[option_index].name);
        }
        break;
      }
      default : {
        // An error message is printed in getopt_long, we just need to exit
        exit(EXIT_FAILURE);
      }
    }
  }

  // If t0 wasn't provided, use default
  if (t0 < 0) {
    t0 = aevol::get_last_gener_from_file();
  }

  // If t_end_ wasn't provided, run for 1000 timesteps
  if (t_end < 0) {
    t_end = t0 + 1000;
  }

  // It the user didn't ask for a parallel run, set number of threads to 1
  #ifdef _OPENMP
  if (not run_in_parallel) {
    omp_set_num_threads(1);
  }
  #endif

  return std::make_tuple(t0, t_end, ui_output_dir, ui_output_frequency, cli_output_frequency, verbose);
}
