// ****************************************************************************
//
//          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 <cstdlib>

#include <filesystem>
#include <format>
#include <fstream>
#include <random>

#include "aevol.h"

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

int main(int argc, char* argv[]) {
  // Print aevol version string
  std::cout << std::format("Running aevol version {}\n", aevol::version_string);

  // Read command line arguments
  const auto [fasta_file_name, param_file_name, nb_mutants, mutation_type, prng_seed, full_output_requested, verbose] =
      interpret_cmd_line_args(argc, argv);

  // Initialize fasta reader
  auto fasta_reader = aevol::FastaReader(fasta_file_name);

  // Initialize the individual analyser from the parameter file
  auto individual_analyser = aevol::IndividualAnalyser::make_from_param_file(param_file_name);
  const auto& phenotypic_target = individual_analyser->target();

  // Initialize PRNG
  auto prng = std::make_unique<aevol::JumpingMT>(prng_seed);

  // Create output directory
  auto output_dir =
      std::filesystem::path("mutagenesis-" + aevol::to_string(mutation_type) + '-' + aevol::time_string());
  std::filesystem::create_directories(output_dir);

  // Open summary output file
  auto summary_output_path = output_dir/std::string("summary.csv");
  auto summary_output = std::ofstream{summary_output_path};
  if (not summary_output) {
    aevol::exit_with_usr_msg(std::string("failed to open output file ") + summary_output_path.string());
  }
  // Write header
  summary_output << "SeqID,nb_neutral_mutants,nb_beneficial_mutants,nb_deleterious_mutants,"
                 << "sum_positive_delta_fitness,sum_negative_delta_fitness,robustness,evolvability\n";

  // Open full output file if it was requested
  auto full_output = std::ofstream{};
  if (full_output_requested) {
    auto path = output_dir/std::string("full.csv");
    full_output.open(path);
    if (not full_output) {
      aevol::exit_with_usr_msg(std::string("failed to open output file ") + path.string());
    }
    // Write header
    full_output << "SeqID,delta_fitness\n";
  }

  // For each individual in fasta file
  while (not fasta_reader.eof()) {
    auto nb_neutral_mutants = size_t{0};
    auto nb_beneficial_mutants = size_t{0};
    auto nb_deleterious_mutants = size_t{0};
    auto sum_positive_delta_fitness = double{0};
    auto sum_negative_delta_fitness = double{0};

    try {
      // Read individual from provided fasta file
      auto [seqid, indiv, modifiers] = fasta_reader.read_individual();

      // Evaluate individual
      indiv->evaluate(aevol::exp_setup->w_max(), aevol::exp_setup->selection_pressure(), phenotypic_target);
      auto original_fitness = indiv->fitness();

      // Generate nb_mutants mutants for the requested mutation type
      // NB: for switches on 2b genomes all possible switches are tested (nb_mutants is overridden)
      auto complete_switch_map = (NB_BASE == 2) and (mutation_type == aevol::MutationEvent::Type::SWITCH);
      auto nb_mutants_to_generate = complete_switch_map ? indiv->dna().length() : nb_mutants;
      for (auto i = decltype(nb_mutants_to_generate){0}; i < nb_mutants_to_generate; ++i) {
        // Create offspring, mutate and evaluate
        auto mutant = aevol::Individual::make_clone(*indiv);

        // Generate the mutation event
        auto individual_mutator =
            std::make_unique<aevol::Individual::mutator_type>(*mutant,
                                                              aevol::exp_setup->mut_params(),
                                                              aevol::exp_setup->genome_limits());
        auto mutation = complete_switch_map
                            ? aevol::MutationEvent::make_switch(i)
                            : individual_mutator->dna_mutator().generate_mutation_event(
                                  mutation_type, *prng, indiv->dna().length());

        // Mutate and evaluate mutant
        mutant->mutable_dna().apply_mutation(*mutation);
        mutant->evaluate(aevol::exp_setup->w_max(), aevol::exp_setup->selection_pressure(), phenotypic_target);

        // Update summary stats according to relative fitness
        auto fitness = mutant->fitness();
        auto delta_fitness = fitness - original_fitness;
        if (fabs(delta_fitness) < 1e-10 * std::max(original_fitness, fitness)) {
          ++nb_neutral_mutants;
        } else if (delta_fitness > 0) {
          ++nb_beneficial_mutants;
          sum_positive_delta_fitness += delta_fitness;
        } else {
          ++nb_deleterious_mutants;
          sum_negative_delta_fitness += delta_fitness;
        }

        if (full_output_requested) {
          full_output << std::format("{} {}\n",
                                     seqid,
                                     delta_fitness);
        }
      }

      summary_output << std::format("{} {} {} {} {} {} {} {}\n",
                                    seqid,
                                    nb_neutral_mutants,
                                    nb_beneficial_mutants,
                                    nb_deleterious_mutants,
                                    sum_positive_delta_fitness,
                                    sum_negative_delta_fitness,
                                    static_cast<double>(nb_neutral_mutants) / nb_mutants,
                                    sum_positive_delta_fitness / nb_mutants);
    } catch(std::exception& e) {
      if (not fasta_reader.eof()) {
        aevol::exit_with_usr_msg(e.what());
      }
    }
  }

  std::cout << std::format("Output written to {}\n", output_dir.string());

  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("{}:\ngenerate single-mutants of the provided individuals, using the requested mutation "
                           "type; output fitness effect stats thereof.\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 : {} [OPTIONS] FASTA_FILE PARAM_FILE MUTATION_TYPE\n", prog_name)
            << "\n"
            <<             "   FASTA_FILE:    sequence(s) of the individual(s) of interest\n"
            <<             "   PARAM_FILE:    parameters to be used for the experiment\n"
            <<             "   MUTATION_TYPE: type of mutations to generate. Possible values:\n"
            <<             "     'SWITCH', 'SMALL_INSERTION', 'SMALL_DELETION' (local mutations)\n"
            <<             "     'DUPLICATION', 'DELETION', 'TRANSLOCATION', 'INVERSION' (rearrangements)\n"
            << "\nOptions\n"
            << "  -h, --help\n\tprint this help, then exit\n"
            << "  -V, --version\n\tprint version number, then exit\n"
            << "  -n, --nb-offspring NB_OFFSPRING\n\tnumber of offspring to be generated for each individual\n"
            << "  -v, --verbose\n\tbe verbose\n"
            << "  --full\n\trequire additional output containing the Fitness Effect of each replication\n"
            << "  --seed PRNG_SEED\n\tseed to be used for prng initialization\n";
}

auto interpret_cmd_line_args(int argc, char* argv[])
    -> std::tuple<std::filesystem::path, std::filesystem::path, size_t, aevol::MutationType, uint32_t, bool, bool> {
  // Command-line option variables
  auto fasta_file_name = std::filesystem::path{};
  auto param_file_name = std::filesystem::path{};
  auto nb_mutants      = size_t{1000};
  auto mutation_type   = aevol::MutationType{};
  auto prng_seed       = uint32_t{0};
  auto seed_provided   = false;
  auto full            = false;
  auto verbose         = false;

  // Define allowed options
  const char* options_list = "hVvn:";
  int option_index = 0;
  enum long_opt_codes {
    FULL_OUTPUT = 1000,
    PRNG_SEED   = 1001
  };
  static struct option long_options_list[] = {
      {"help",          no_argument,       nullptr, 'h'},
      {"version",       no_argument,       nullptr, 'V'},
      {"verbose",       no_argument,       nullptr, 'v'},
      {"nb-mutants",    required_argument, nullptr, 'n'},
      // long-only options
      {"full",          no_argument,       nullptr, FULL_OUTPUT},
      {"seed",          required_argument, nullptr, PRNG_SEED},
      {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 'v' : {
        verbose = true;
        break;
      }
      case 'n' : {
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), nb_mutants);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg(std::string("invalid value for option ") + long_options_list[option_index].name);
        }
        break;
      }
      // Handle long-only options
      case FULL_OUTPUT: {
        full = true;
        break;
      }
      case PRNG_SEED: {
        seed_provided = true;
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), prng_seed);
        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);
      }
    }
  }

  // Check number of positional arguments
  if (argc != optind + 3) {
    std::cerr << std::format("{0}: incorrect number of positional arguments\n"
                             "Try '{0} --help' for more information.\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // Generate random prng seed if none was provided
  if (not seed_provided) {
    prng_seed = std::random_device{}();
    std::cout << std::format("No prng seed provided, using random seed: {}\n", prng_seed);
  }

  // Interpret positional arguments
  fasta_file_name = argv[optind];
  param_file_name = argv[optind + 1];
  mutation_type   = aevol::to_MutationType(argv[optind + 2]);

  return {fasta_file_name, param_file_name, nb_mutants, mutation_type, prng_seed, full, verbose};
}
