// ****************************************************************************
//
//          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 "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,
                                                                   std::optional<aevol::time_type>,
                                                                   std::optional<aevol::indiv_id_type>,
                                                                   std::filesystem::path,
                                                                   bool,
                                                                   bool>;
auto make_fasta_output_path(aevol::time_type t0,
                            std::optional<aevol::time_type> t_end,
                            std::optional<aevol::indiv_id_type> indiv_index) -> std::filesystem::path;
auto make_gff_output_path(aevol::time_type t, std::optional<aevol::indiv_id_type> indiv_index) -> std::filesystem::path;

int main(int argc, char* argv[]) {
  const auto [t0, t_end, indiv_index, output_path, gff, 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);

  // Open output file
  auto output = aevol::FastaWriter(output_path);
  if (not output) {
    aevol::exit_with_usr_msg(std::string("failed to open output file ") + output_path.string());
  }

  // Load successive checkpoints from t0 to t_end and write the corresponding genomes
  auto t = t0;
  do {
    // Load the simulation
    auto checkpoint_explorer = aevol::CheckpointExplorer::make_from_checkpoint(t);

    // Write genome to fasta file
    const auto& indiv_of_interest =
        indiv_index ? checkpoint_explorer->individual(*indiv_index) : checkpoint_explorer->best_indiv();
    auto [seqid, modifiers] = checkpoint_explorer->get_fasta_annotations(&indiv_of_interest);
    output.write_individual(seqid, indiv_of_interest, modifiers);

    // Write gff if requested
    if (gff) {
      auto gff_output_path = make_gff_output_path(t, indiv_index);
      aevol::gff3::write_individual_gff3(seqid, indiv_of_interest, gff_output_path);
      std::cout << std::format("Annotated genome written to {}\n", gff_output_path.string());
    }

    t += checkpoint_explorer->checkpoint_frequency();
  } while (t_end and t <= t_end);

  std::cout << std::format("Genomes written to {}\n", output_path.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("{}: extract genome(s) from aevol checkpoints.\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] [--gff] [-v]\n", prog_name)
            << std::format("   or : {} [-b TIMESTEP] [-i INDIV_INDEX] [--gff] [-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 timestep from which to extract the first genome(s) (default: 0)\n"
            << "  -e, --end TIMESTEP\n"
            << "\tif specified, a genome will be extracted at each checkpoint\n"
            << "\tfrom --begin to --end included\n"
            << "  -i, --index INDIV_INDEX\n"
            << "\tif specified, extract genome of individual with provided index,\n"
            << "\totherwise extract best individual\n"
            << "  --gff\n"
            << "\tif present, a gff3 file will be output for each extracted genome\n"
            << "  -v, --verbose\n\tbe verbose\n";
}

auto interpret_cmd_line_args(int argc, char* argv[]) -> std::tuple<aevol::time_type,
                                                                   std::optional<aevol::time_type>,
                                                                   std::optional<aevol::indiv_id_type>,
                                                                   std::filesystem::path,
                                                                   bool,
                                                                   bool> {
  // Command-line option variables
  auto t0      = aevol::time_type{0};
  auto t_end   = std::optional<aevol::time_type>{std::nullopt};
  auto indiv_index = std::optional<aevol::indiv_id_type>{std::nullopt};
  auto gff_output_requested = bool{false};
  auto verbose = bool{false};

  // Define allowed options
  const char* options_list = "hVb:e:i:v";
  int option_index = 0;
  enum long_opt_codes {
    GFF3 = 1000
  };
  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'},
      {"index",         required_argument, nullptr, 'i'},
      {"verbose",       no_argument,       nullptr, 'v'},
      // long-only options
      {"gff",           no_argument, nullptr, GFF3},
      {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("invalid value for option -b or --begin");
        }
        break;
      }
      case 'e' : {
        t_end = -1;
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), *t_end);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg("invalid value for option -e or --end");
        }
        break;
      }
      case 'i' : {
        indiv_index = -1;
        auto [ptr, ec] = std::from_chars(optarg, optarg + strlen(optarg), *indiv_index);
        if (ec != std::errc()) {
          aevol::exit_with_usr_msg("invalid value for option -i or --index");
        }
        break;
      }
      case 'v' : {
        verbose = true;
        break;
      }
        // Handle long-only options
      case GFF3: {
        gff_output_requested = true;
        break;
      }
      default : {
        // An error message is printed in getopt_long, we just need to exit
        exit(EXIT_FAILURE);
      }
    }
  }

  // Options -i and -e are incompatible. Check they were not both used
  if (t_end and indiv_index) {
    aevol::exit_with_usr_msg(std::string("options --end (or -e) and --index (or -i) cannot be used together"));
  }

  auto output_path = make_fasta_output_path(t0, t_end, indiv_index);

  return std::make_tuple(t0, t_end, indiv_index, output_path, gff_output_requested, verbose);
}

auto make_fasta_output_path(aevol::time_type t0,
                            std::optional<aevol::time_type> t_end,
                            std::optional<aevol::indiv_id_type> indiv_index) -> std::filesystem::path {
  auto path = std::string("genome");
  path += t_end ? std::format("_{:0>9}_{:0>9}", t0, *t_end) : std::format("_{:0>9}", t0);
  path += indiv_index ? std::format("_i{}", *indiv_index) : "_best";
  path += ".fa";
  return std::filesystem::path{path};
}

auto make_gff_output_path(aevol::time_type t, std::optional<aevol::indiv_id_type> indiv_index)
    -> std::filesystem::path {
  if (not indiv_index) {
    return std::filesystem::path{std::format("genome_{:0>9}_best.gff3", t)};
  } else {
    return std::filesystem::path{std::format("genome_{:0>9}_i{}.gff3", t, *indiv_index)};
  }
}
