// ****************************************************************************
//
//          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 "Dna.h"

#include <algorithm>
#include <cassert>
#include <cstring>
#include <ranges>

#include "AnnotatedChromosome.h"
#include "biochemistry/mutations/mutators/DnaMutator.h"
#include "DnaFactory.h"
#include "ExpSetup.h"

#include "Deletion.h"
#include "Duplication.h"
#include "Inversion.h"
#include "macros.h"
#include "PointMutation.h"
#include "SmallDeletion.h"
#include "SmallInsertion.h"
#include "Translocation.h"

#define REDUCTION_FACTOR 16

namespace aevol {

Dna::Dna(size_type length) {
    length_ = length;

    nb_blocks_ = nb_required_blocks(length_);
    auto status = posix_memalign((void **) &data_, 64, nb_blocks_ * BLOCK_SIZE * sizeof(char));//new char[nb_blocks_ * BLOCK_SIZE];
    check_posix_memalign(status);

    memset(data_, 0, (length_ + 1) * sizeof(char));
}

Dna* Dna::make_dna(size_t request_size) {
  return dna_factory->get_dna(request_size);
}

Dna* Dna::make_from_sequence(const std::string& seq) {
  auto dna = dna_factory->get_dna(seq.size());
  dna->reset_sequence(seq.data(), seq.size());
  return dna;
}

Dna* Dna::make_recombinant(const Dna& dna1, Dna::size_type pos1, const Dna& dna2, Dna::size_type pos2) {
  auto recombinant_size = pos1 + dna2.length() - pos2;
  auto dna = Dna::make_dna(recombinant_size);

  // Copy sequences
  memcpy(dna->data_, dna1.data_, pos1 * sizeof(*dna1.data_));
  memcpy(&(dna->data_[pos1]),
         &(dna2.data_[pos2]),
         (dna2.length() - pos2) + 1 * sizeof(*dna1.data_)); // + 1 for '\0'

  return dna;
}

void Dna::release(Dna*&& dna) {
  dna_factory->give_back(dna);
}

void Dna::reserve(size_type new_capacity) {
  auto new_nb_blocks = nb_required_blocks(new_capacity);

  if ((new_nb_blocks > nb_blocks_) || (new_nb_blocks < nb_blocks_ / REDUCTION_FACTOR)) {
    nb_blocks_ = new_nb_blocks;
    if (data_ != nullptr) {
      free(data_);
      data_ = nullptr;
    }

    auto status = posix_memalign((void **) &data_, 64, nb_blocks_ * BLOCK_SIZE * sizeof(char));
    check_posix_memalign(status);

    memset(data_, 0, (new_capacity + 1) * sizeof(char));
  }
}

void Dna::reset_sequence(const char* seq, size_type len) {
  reserve(len);
  length_ = len;
  memcpy(data_, seq, (length_+1) * sizeof(char));
}

void Dna::reset_length(size_type length) {
    reserve(length);
    length_ = length;
}

void Dna::set_chrsm(AnnotatedChromosome* ann_chrsm){
  ann_chrsm_ = ann_chrsm;
}

char Dna::get_lead(size_type pos) const {
  assert(not exp_setup->linear_chrsm() or pos < length_);
  return data_[mod(pos, length_)];
}

char Dna::get_lag(size_type pos) const {
  assert(not exp_setup->linear_chrsm() or pos < length_);
  return get_complementary_base(data_[mod(pos, length_)]);
}

Dna::~Dna() {
  if (data_ != nullptr) { free(data_); data_=nullptr;}

if (mutation_list.size() > 0) {
    for (auto repl : mutation_list)
        delete repl;
    mutation_list.clear();
}


}

void Dna::remove(size_type pos_1, size_type pos_2) {
  assert(pos_1 >= 0 && pos_2 >= pos_1 && pos_2 <= length_);

  // Compute size of new genome
  auto new_length = length_ - (pos_2 - pos_1);

  Dna* new_genome = Dna::make_dna(new_length);
  new_genome->set_chrsm(ann_chrsm_);
  new_genome->reset_length(new_length);

  // Copy the remaining of the genome in tmp (preceeding and following parts)
  memcpy(new_genome->data_, data_, pos_1 * sizeof(char));
  memcpy(&(new_genome->data_[pos_1]), &data_[pos_2],
         (new_length - pos_1) * sizeof(char));
  new_genome->data_[new_length] = '\0';

  // Replace previous genome with the new one
  char* old_data = data_;
    int new_nb_block = new_genome->nb_blocks_;
    new_genome->nb_blocks_ = nb_blocks_;
    data_ = new_genome->data_;
  new_genome->data_ = old_data;
  new_genome->length_ = length();

  release(std::move(new_genome));

  // Update length data
  length_ = new_length;
  nb_blocks_ = new_nb_block;
}

void Dna::insert(size_type pos, const char* seq, size_type seq_length) {
// Insert sequence 'seq' at position 'pos'
  assert(pos >= 0 && (pos < length_ || (exp_setup->linear_chrsm() && pos <= length_)));

  // If the sequence's length was not provided, compute it
  if (seq_length == -1) {
    seq_length = strlen(seq);
  }

  // Compute size of new genome
  auto new_length = length_ + seq_length;

  Dna* new_genome = Dna::make_dna(new_length);
  new_genome->set_chrsm(ann_chrsm_);
  new_genome->reset_length(new_length);

  // Build new genome from previous genome and sequence to insert
  memcpy(new_genome->data_, data_, pos * sizeof(char));
  memcpy(&(new_genome->data_[pos]), seq, seq_length * sizeof(char));
  memcpy(&(new_genome->data_[pos+seq_length]), &data_[pos],
         (length_ - pos) * sizeof(char));
  new_genome->data_[new_length] = '\0';

  // Replace the previous genome with the new one
    char* old_data = data_;
    data_ = new_genome->data_;
    int new_nb_block = new_genome->nb_blocks_;
    new_genome->data_ = old_data;
    new_genome->nb_blocks_ = nb_blocks_;
    new_genome->length_ = length();

    release(std::move(new_genome));

  // Update length-related data
  length_ = new_length;
  nb_blocks_ = new_nb_block;
}

void Dna::replace(size_type pos, char* seq, size_type seq_length) {
// Invert the sequence between positions 'first' and 'last'
  // Check pos value
  assert(pos >= 0 && pos < length_);

  // If the sequence's length was not provided, compute it
  if (seq_length == -1) {
    seq_length = strlen(seq);
  }

  // Check that the sequence is contiguous
  assert(pos + seq_length <= length_);

  // Perform the replacement
  memcpy(&data_[pos], seq, seq_length * sizeof(char));
}

#ifdef BASE_2
auto Dna::do_switch(size_type pos) -> std::unique_ptr<MutationReport> {
#else
auto Dna::do_switch(size_type pos, char new_base) -> std::unique_ptr<MutationReport> {
#endif
  #ifdef BASE_2
  // Perform the mutation
  if (data_[pos] == '0') data_[pos] = '1';
  else data_[pos] = '0';
  #elif BASE_4
  // Perform the mutation
  data_[pos] = new_base;
  #endif

  // Remove promoters containing the switched base
  if (exp_setup->linear_chrsm()) {
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos, pos + 1, length_);
  } else {
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos, mod(pos + 1, length_), length_);
  }

  // Look for potential new promoters containing the switched base
  if (length() >= PROM_SIZE) {
    if (exp_setup->linear_chrsm()) {
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos, pos + 1);
    } else {
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos, mod(pos + 1, length()));
    }
  }

  #ifdef BASE_2
  return std::make_unique<PointMutation>(pos);
  #elif BASE_4
  return std::make_unique<PointMutation>(pos, new_base);
  #endif
}

auto Dna::do_small_insertion(size_type pos, int16_t nb_insert, const char* seq) -> std::unique_ptr<MutationReport> {
  // Remove the promoters that will be broken
  ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos, length_);

  insert(pos, seq, nb_insert);

  // Look for new promoters
  if (length_ >= PROM_SIZE) {
    if (length_ - nb_insert < PROM_SIZE) {
      // Special case where the genome was smaller than a promoter before the
      // insertion and greater than (or as big as) a promoter after the
      // insertion.
      // In that case, we must look for new promoters thoroughly on the whole
      // genome using locate_promoters
      ann_chrsm_->mutable_promoter_list().locate_promoters(*this);
    }
    else {
      ann_chrsm_->mutable_promoter_list().move_all_promoters_after(pos, nb_insert, length_);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos, pos + nb_insert);
    }
  }

  return std::make_unique<SmallInsertion>(pos, nb_insert, seq);
}

auto Dna::do_small_deletion(size_type pos, int16_t nb_del) -> std::unique_ptr<MutationReport> {
  auto old_pos = pos;

  // Remove promoters containing at least one nucleotide from the sequence to  delete
  auto end_del = size_type{0};
  if (exp_setup->linear_chrsm()) {
    end_del = std::min(length(), pos + nb_del);
  } else {
    end_del = mod(pos + nb_del, length());
  }

  ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos, end_del, length());

  if (exp_setup->linear_chrsm()) {
    assert(pos + nb_del <= length());
  }

  // Do the deletion and update promoter list
  if (pos + nb_del <= length()) { // the deletion does not contain the origin of
    // replication
    // Do the deletion

    remove(pos, pos + nb_del);

    // Update promoter list
    if (length() >= PROM_SIZE) {
      ann_chrsm_->mutable_promoter_list().move_all_promoters_after(pos, -nb_del, length_);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, mod(pos, length()));
    }
  }
  else { // the deletion contains the origin of replication
    // Do the deletion
    int32_t nb_del_at_pos_0 = nb_del - length() + pos;

    remove(pos, length_);
    remove(0, nb_del_at_pos_0);


    pos -= nb_del_at_pos_0;

    // Update promoter list
    if (length() >= PROM_SIZE) {
      ann_chrsm_->mutable_promoter_list().move_all_promoters_after(0, -nb_del_at_pos_0, length_);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, 0);
    }
  }

  return std::make_unique<SmallDeletion>(old_pos, nb_del);
}

auto Dna::do_duplication(size_type pos_1, size_type pos_2, size_type pos_3) -> std::unique_ptr<MutationReport> {
  // Duplicate segment [pos_1; pos_2[ and insert the duplicate before pos_3
  char* duplicate_segment = NULL;

    if (length_ == 1)
    {
        printf("*** genome of size 1 ; duplication not done *** \n");
        return nullptr;
    }

  size_type seg_length;

  if (exp_setup->linear_chrsm()) {
    assert(pos_1 < pos_2);
  }
  if (pos_1 < pos_2) {
    //
    //       pos_1         pos_2                   -> 0-
    //         |             |                   -       -
    // 0--------------------------------->      -         -
    //         ===============                  -         - pos_1
    //           tmp (copy)                      -       -
    //                                             -----      |
    //                                             pos_2    <-'
    //
    seg_length = pos_2 - pos_1;

    auto status = posix_memalign((void **)&duplicate_segment,64,(seg_length+1)* sizeof(char));//new char[seg_length + 1];
    check_posix_memalign(status);

    memcpy(duplicate_segment, &data_[pos_1], seg_length);
    duplicate_segment[seg_length] = '\0';

  }
  else { // if (pos_1 >= pos_2)
    // The segment to duplicate includes the origin of replication.
    // The copying process will be done in two steps.
    //
    //                                            ,->
    //    pos_2                 pos_1            |      -> 0-
    //      |                     |                   -       - pos_2
    // 0--------------------------------->     pos_1 -         -
    // ======                     =======            -         -
    //  tmp2                        tmp1              -       -
    //                                                  -----
    //
    //
    auto tmp1_len = length() - pos_1;
    auto tmp2_len = pos_2;
    seg_length = tmp1_len + tmp2_len;

    auto status = posix_memalign((void **)&duplicate_segment,64,(seg_length+1)* sizeof(char));//new char[seg_length + 1];
    check_posix_memalign(status);

    memcpy(duplicate_segment, &data_[pos_1], tmp1_len);     // Copy tmp1
    memcpy(&duplicate_segment[tmp1_len], data_, tmp2_len);  // Copy tmp2
    duplicate_segment[seg_length] = '\0';
  }

  if (seg_length <= 0) { // TODO<dpa> should never happen right?
    free(duplicate_segment);
    return nullptr;
  }

  // Create a copy of the promoters born by the segment to be duplicated
  // (they will be inserted in the individual's promoter list later)
  PromoterList duplicated_promoters = ann_chrsm_->mutable_promoter_list().duplicate_promoters_included_in(pos_1, pos_2, length_);

  ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_3, length_);

  insert(pos_3, duplicate_segment, seg_length);
  
  if (length() >= PROM_SIZE) {
    if (length() - seg_length < PROM_SIZE) {
      // Special case where the genome was smaller than a promoter before
      // the insertion and greater than (or as big as) a promoter after the
      // insertion.
      // In that case, we must look for new promoters thoroughly on the whole
      // genome using locate_promoters
      ann_chrsm_->mutable_promoter_list().locate_promoters(*this);
    }
    else {
      ann_chrsm_->mutable_promoter_list().move_all_promoters_after(pos_3, seg_length, length_);

      duplicated_promoters.shift_promoters(pos_3, length_);
      ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(duplicated_promoters));

      if (exp_setup->linear_chrsm()) {
        if (pos_3 != 0) {
          ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_3);
        }
        if (pos_3 + seg_length != length_) {
          ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_3 + seg_length);
        }
      } else {
        ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_3);
        ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_3 + seg_length);
      }
    }
  }

  free(duplicate_segment);

  return std::make_unique<Duplication>(pos_1, pos_2, pos_3, seg_length);
}

auto Dna::do_translocation(size_type pos_1, size_type pos_2, size_type pos_3, size_type pos_4, bool invert)
    -> std::unique_ptr<MutationReport> {
  auto pos_min = std::min(pos_1, std::min(pos_2, std::min(pos_3, pos_4)));

  if (not invert) {
    if (pos_min == pos_1) {
      ABCDE_to_ADCBE(pos_1, pos_3, pos_2, pos_4);
    }
    else if (pos_min == pos_2) {
      ABCDE_to_ADCBE(pos_2, pos_4, pos_1, pos_3);
    }
    else if (pos_min == pos_3) {
      ABCDE_to_ADCBE(pos_3, pos_2, pos_4, pos_1);
    }
    else { // if (pos_min == pos_4)
      ABCDE_to_ADCBE(pos_4, pos_1, pos_3, pos_2);
    }
  }
  else { // invert
    if (pos_min == pos_1) {
      ABCDE_to_ADBpCpE(pos_1, pos_3, pos_2, pos_4);
    }
    else if (pos_min == pos_2) {
      ABCDE_to_ADBpCpE(pos_2, pos_4, pos_1, pos_3);
    }
    else if (pos_min == pos_3) {
      ABCDE_to_ACpDpBE(pos_3, pos_2, pos_4, pos_1);
    }
    else { // if (pos_min == pos_4)
      ABCDE_to_ACpDpBE(pos_4, pos_1, pos_3, pos_2);
    }
  }

  auto segment_length = pos_2 - pos_1;

  return std::make_unique<Translocation>(pos_1, pos_2, pos_3, pos_4, segment_length, invert);
}

auto Dna::do_inversion(size_type pos_1, size_type pos_2) -> std::unique_ptr<MutationReport> {
// Invert segment going from pos_1 (included) to pos_2 (excluded)
// Exemple : sequence 011101001100 => 110011010001
  if (pos_1 == pos_2) return nullptr; // Invert everything <=> Invert nothing!
  //
  //       pos_1         pos_2                   -> 0-
  //         |             |                   -       -
  // 0--------------------------------->      -         -
  //         ===============                  -         - pos_1
  //           tmp (copy)                      -       -
  //                                             -----      |
  //                                             pos_2    <-'
  //

  if (exp_setup->linear_chrsm()) {
    assert(pos_1 <= pos_2);
  }

        if (length_ == 1)
        {
            printf("*** genome of size 1 ; inversion not done *** \n");
            return nullptr;
        }

  auto seg_length = pos_2 - pos_1;

  // Create the inverted sequence
  char* inverted_segment = NULL;
  auto status = posix_memalign((void **)&inverted_segment,64,(seg_length+1)* sizeof(char));//new char[seg_length + 1];
  check_posix_memalign(status);

#ifdef BASE_2
#if defined(__INTEL_COMPILER) || defined(__INTEL_LLVM_COMPILER)
#pragma omp simd aligned(data_:64) aligned(inverted_segment:64)
#else
#pragma omp simd
#endif
#endif
  for (decltype(seg_length) i = 0; i < seg_length; i++) {
    int32_t j = pos_2 - 1 - i;
    inverted_segment[i] = get_complementary_base(data_[j]);
  }
  inverted_segment[seg_length] = '\0';

  // Remove promoters that included a breakpoint
  ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_1, length_);
  ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_2, length_);

  // Invert the sequence
  replace(pos_1, inverted_segment, seg_length);

  // Update promoter list
  if (length() >= PROM_SIZE) {
    ann_chrsm_->mutable_promoter_list().invert_promoters_included_in(pos_1, pos_2);
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_1);
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_2);
  }

  free(inverted_segment);

  return std::make_unique<Inversion>(pos_1, pos_2, seg_length);
}

auto Dna::do_deletion(size_type pos_1, size_type pos_2) -> std::unique_ptr<MutationReport> {
  if (exp_setup->linear_chrsm()) {
    assert(pos_1 <= pos_2);
  }

  // Delete segment going from pos_1 (included) to pos_2 (excluded)
    if (length_ == 1)
    {
        printf("*** genome of size 1 ; deletion not done *** \n");
        return nullptr;
    }

  if (pos_1 < pos_2) {
    //
    //       pos_1         pos_2                   -> 0-
    //         |             |                   -       -
    // 0--------------------------------->      -         -
    //         ===============                  -         - pos_1
    //           tmp (copy)                      -       -
    //                                             -----      |
    //                                             pos_2    <-'
    //

    auto segment_length = pos_2 - pos_1;

    // Remove promoters containing at least one nucleotide from the sequence
    // to delete
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_1, pos_2, length_);
    // Delete the sequence between pos_1 and pos_2
    remove(pos_1, pos_2);

    // Update promoter list
    if (length() >= PROM_SIZE) {
      ann_chrsm_->mutable_promoter_list().move_all_promoters_after(pos_1, -segment_length, length_);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, pos_1);
    }


  }
  else { // if (pos_1 >= pos_2)
    // The segment to delete includes the origin of replication.
    // The deletion process will be done in two steps.
    //
    //                                            ,->
    //    pos_2                 pos_1            |      -> 0-
    //      |                     |                   -       - pos_2
    // 0--------------------------------->     pos_1 -         -
    // =====                      =======            -         -
    //  tmp2                        tmp1              -       -
    //                                                  -----
    //
    //

    // Remove promoters containing at least one nucleotide from the sequence to delete
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_1, pos_2, length_);

    // Delete the sequence between pos_1 and pos_2
    remove(pos_1, length_); // delete tmp1 from genome
    remove(0, pos_2);       // delete tmp2 from genome
    // Update promoter list
    if (length() >= PROM_SIZE) {
      ann_chrsm_->mutable_promoter_list().move_all_promoters_after(0, -pos_2, length_);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, 0);
    }
  }

  return std::make_unique<Deletion>(pos_1, pos_2, mod(pos_2 - pos_1 - 1, length()) + 1);
}

void Dna::undergo_mutation(const MutationReport& mut) {
  switch (mut.mut_type()) {
    case MutationReport::Type::SWITCH : {
    #ifdef BASE_2
      do_switch(dynamic_cast<const PointMutation&>(mut).pos());
    #elif BASE_4
      const auto& pmut = dynamic_cast<const PointMutation&>(mut);
      // printf("%d -- DO Switch %d : %c\n",AeTime::time(),pmut.pos(), pmut.base());
      do_switch(pmut.pos(), pmut.base());
    #endif
      break;
    }
    case MutationReport::Type::S_INS : {
      const auto& s_ins = dynamic_cast<const SmallInsertion&>(mut);
      // printf("%d -- DO Insert %d Nb %d\n",AeTime::time(),s_ins.length(), s_ins.seq());
      do_small_insertion(s_ins.pos(), s_ins.length(), s_ins.seq());
      break;
    }
    case MutationReport::Type::S_DEL : {
      const auto& s_del = dynamic_cast<const SmallDeletion&>(mut);
      // printf("%d -- DO Delete %d Nb %d\n",AeTime::time(),s_del.pos(), s_del.length());
      do_small_deletion(s_del.pos(), s_del.length());
      break;
    }
    case MutationReport::Type::DUPL : {
      const auto& dupl = dynamic_cast<const Duplication&>(mut);
      // printf("%d -- DO Duplication %d %d %d\n",AeTime::time(),dupl.pos1(), dupl.pos2(), dupl.pos3());
      do_duplication(dupl.pos1(), dupl.pos2(), dupl.pos3());
      break;
    }
    case MutationReport::Type::DEL : {
      const auto& del = dynamic_cast<const Deletion&>(mut);
      // printf("%d -- DO Deletion %d %d\n",AeTime::time(),del.pos1(), del.pos2());

      do_deletion(del.pos1(), del.pos2());
      break;
    }
    case MutationReport::Type::TRANS : {
      const auto& trans = dynamic_cast<const Translocation&>(mut);
      // printf("%d -- DO Translocation %d %d %d %d (%d)\n",AeTime::time(),trans.pos1(), trans.pos2(), trans.pos3(), trans.pos4(),
      //                  trans.invert());
      do_translocation(trans.pos1(), trans.pos2(), trans.pos3(), trans.pos4(),
                       trans.invert());
      break;
    }
    case MutationReport::Type::INV : {
      const auto& inv = dynamic_cast<const Inversion&>(mut);
      // printf("%d -- DO Inversion %d %d\n",AeTime::time(),inv.pos1(), inv.pos2());
      do_inversion(inv.pos1(), inv.pos2());
      break;
    }
    default :
      fprintf(stderr, "ERROR, invalid mutation type in file %s:%d\n",
              __FILE__, __LINE__);
      exit(EXIT_FAILURE);
      break;
  }
}

auto Dna::apply_mutation(const MutationEvent& mutation_to_apply) -> std::unique_ptr<MutationReport> {
  auto undergone_mutation = decltype(apply_mutation(mutation_to_apply)){};
  switch (mutation_to_apply.type()) {
    case MutationEvent::Type::SWITCH: {
      #ifdef BASE_2
      undergone_mutation = do_switch(mutation_to_apply.pos_1());
      #elif BASE_4
      // Determine new_base (basically "new_base = old_base + offset_ % 4" but we handle characters '0' to '3')
      char new_base = '0' + ((data_[mutation_to_apply.pos_1()] - '0' + mutation_to_apply.offset()) % NB_BASE);

      // Perform mutation
      undergone_mutation = do_switch(mutation_to_apply.pos_1(), new_base);
      #endif
      if (undergone_mutation) {
        mutation_metrics_.nb_switch++;
        mutation_metrics_.nb_local_mutations++;
      }
      break;
    }
    case MutationEvent::Type::SMALL_INSERTION: {
      undergone_mutation = do_small_insertion(mutation_to_apply.pos_1(),
                                              mutation_to_apply.number(),
                                              mutation_to_apply.seq());
      if (undergone_mutation) {
        mutation_metrics_.nb_indels++;
        mutation_metrics_.nb_local_mutations++;
      }
      break;
    }
    case MutationEvent::Type::SMALL_DELETION: {
      undergone_mutation = do_small_deletion(mutation_to_apply.pos_1(), mutation_to_apply.number());
      if (undergone_mutation) {
        mutation_metrics_.nb_indels++;
        mutation_metrics_.nb_local_mutations++;
      }
      break;
    }
    case MutationEvent::Type::DUPLICATION: {
      undergone_mutation = do_duplication(mutation_to_apply.pos_1(),
                                          mutation_to_apply.pos_2(),
                                          mutation_to_apply.pos_3());
      if (undergone_mutation) {
        mutation_metrics_.nb_large_dupl++;
        mutation_metrics_.nb_rearrangements++;
      }
      break;
    }
    case MutationEvent::Type::TRANSLOCATION: {
      undergone_mutation = do_translocation(mutation_to_apply.pos_1(),
                                            mutation_to_apply.pos_2(),
                                            mutation_to_apply.pos_3(),
                                            mutation_to_apply.pos_4(),
                                            mutation_to_apply.invert());
      if (undergone_mutation) {
        mutation_metrics_.nb_large_trans++;
        mutation_metrics_.nb_rearrangements++;
      }
      break;
    }
    case MutationEvent::Type::INVERSION: {
      undergone_mutation = do_inversion(mutation_to_apply.pos_1(), mutation_to_apply.pos_2());
      if (undergone_mutation) {
        mutation_metrics_.nb_large_inv++;
        mutation_metrics_.nb_rearrangements++;
      }
      break;
    }
    case MutationEvent::Type::DELETION: {
      undergone_mutation = do_deletion(mutation_to_apply.pos_1(), mutation_to_apply.pos_2());
      if (undergone_mutation) {
        mutation_metrics_.nb_large_del++;
        mutation_metrics_.nb_rearrangements++;
      }
      break;
    }
  }

  return undergone_mutation;
}

auto Dna::apply_mutations(const std::list<std::unique_ptr<MutationEvent>>& mutations_to_apply)
    -> std::list<std::unique_ptr<MutationReport>> {
  decltype(apply_mutations(mutations_to_apply)) undergone_mutations;

  for (const auto& mut_to_apply: mutations_to_apply) {
    if (mut_to_apply != nullptr) {
      if (auto undergone_mutation = apply_mutation(*mut_to_apply)) {
        undergone_mutations.push_back(std::move(undergone_mutation));
      }
    }
  }

  return undergone_mutations;
}


void Dna::ABCDE_to_ADCBE(size_type pos_B, size_type pos_C, size_type pos_D, size_type pos_E) {
  // Rearrange the sequence from ABCDE to ADCBE (complex translocation
  // of segment defined between positions pos_B and pos_D)
  //
  // Segments are identified by pos_x values as shown below.
  //
  // TODO(dpa) CHECK THIS !!!
  // WARNING : Segment C includes nucleotide at pos_D // NOTE : WTF???
  //
  //         A      B        C       D       E
  //      |----->=======[>=======>-------[>-------|        =>
  //          pos_B   pos_C    pos_D   pos_E
  //
  //                         |
  //                         V
  //
  //         A      D        C       B        E
  //      |----->-------[>=======>=======[>-------|
  // Compute segment lengths
  auto len_A = pos_B;
  auto len_B = pos_C - pos_B;
  auto len_C = pos_D - pos_C;
  auto len_D = pos_E - pos_D;
  auto len_E = length() - pos_E;
  auto len_AD = len_A + len_D;
  auto len_ADC = len_AD + len_C;
  auto len_ADCB = len_ADC + len_B;

  // Create new sequence
  Dna* new_genome = Dna::make_dna(length_);

    new_genome->set_chrsm(ann_chrsm_);
    new_genome->reset_length(length_);

  memcpy(new_genome->data_, data_, len_A * sizeof(char));
  memcpy(&(new_genome->data_[len_A]), &data_[pos_D], len_D * sizeof(char));
  memcpy(&(new_genome->data_[len_AD]), &data_[pos_C], len_C * sizeof(char));
  memcpy(&(new_genome->data_[len_ADC]), &data_[pos_B], len_B * sizeof(char));
  memcpy(&(new_genome->data_[len_ADCB]), &data_[pos_E], len_E * sizeof(char));
  new_genome->data_[length_] = '\0';

  // Replace sequence
  // NB : The size of the genome doesn't change. Therefore, we don't nee
  // to update length_ and nb_blocks_
    char* old_data = data_;
    data_ = new_genome->data_;
    int new_nb_block = new_genome->nb_blocks_;
    new_genome->nb_blocks_ = nb_blocks_;

    new_genome->data_ = old_data;
    new_genome->length_ = length();

    nb_blocks_ = new_nb_block;
    release(std::move(new_genome));


  // ========== Update promoter list ==========
  if (length() >= PROM_SIZE) {
    // Remove promoters that include a breakpoint
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_B, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_C, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_D, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_E, length_);

    // Create temporary lists for promoters to move and/or invert
    PromoterList promoters_B;
    PromoterList promoters_C;
    PromoterList promoters_D;

    // Extract promoters that are totally included in each segment to be moved
    // and shift them to their new positions
    if (len_B >= PROM_SIZE) {
      promoters_B = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_B, pos_C);
      promoters_B.shift_promoters(len_D + len_C, length());
    }
    if (len_C >= PROM_SIZE) {
      promoters_C = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_C, pos_D);
      promoters_C.shift_promoters(len_D - len_B, length());
    }
    if (len_D >= PROM_SIZE) {
      promoters_D = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_D, pos_E);
      promoters_D.shift_promoters(-len_B - len_C, length());
    }

    // Reinsert the shifted promoters
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_B));
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_C));
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_D));

    // 5) Look for new promoters including a breakpoint
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_A);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_AD);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_ADC);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_ADCB);
  }
}

void Dna::ABCDE_to_ADBpCpE(size_type pos_B, size_type pos_C, size_type pos_D, size_type pos_E) {
  // Rearrange the sequence from ABCDE to ADBpCpE (complex translocation
  // with inversion of segment defined between positions pos_B and pos_D)
  // Bp (resp Cp) stands for inverted B (resp C)
  //
  // Segments are identified by pos_x values as shown below.
  //
  // TODO(dpa) CHECK THIS !!!
  // WARNING : Segment C includes nucleotide at pos_D // NOTE : WTF???
  //
  //         A      B        C       D        E
  //      |----->=======[>=======>-------<]-------|
  //          pos_B   pos_C    pos_D   pos_E
  //
  //                         |
  //                         V
  //
  //         A      D        Bp      Cp       E
  //      |----->-------<]=======<=======<]-------|
  // Compute segment lengths

  auto len_A = pos_B;
  auto len_B = pos_C - pos_B;
  auto len_C = pos_D - pos_C;
  auto len_D = pos_E - pos_D;
  auto len_E = length() - pos_E;
  auto len_AD = len_A + len_D;
  auto len_ADB = len_AD + len_B;
  auto len_ADBC = len_ADB + len_C;

  // Create new sequence
  Dna* new_genome = Dna::make_dna(length_);
    new_genome->set_chrsm(ann_chrsm_);
    new_genome->reset_length(length_);

  // Copy segments A and D
  memcpy(new_genome->data_, data_, len_A * sizeof(char));
  memcpy(&(new_genome->data_[len_A]), &data_[pos_D], len_D * sizeof(char));


  // Build Bp and put it in the new genome
  char* inverted_segment;
  auto status = posix_memalign((void **)&inverted_segment,64,(len_B+1)* sizeof(char));//new char[len_B + 1];
  check_posix_memalign(status);

  for (size_type i = 0, j = pos_C - 1; i < len_B; i++, j--) {
    inverted_segment[i] = get_complementary_base(data_[j]);
  }
  inverted_segment[len_B] = '\0';

  memcpy(&(new_genome->data_[len_AD]), inverted_segment, len_B * sizeof(char));

  free(inverted_segment);


  // Build Cp and put it in the new genome
  status = posix_memalign((void **)&inverted_segment,64,(len_C+1)* sizeof(char));//new char[len_C + 1];
  check_posix_memalign(status);

  for (size_type i = 0, j = pos_D - 1; i < len_C; i++, j--) {
    inverted_segment[i] = get_complementary_base(data_[j]);
  }
  inverted_segment[len_C] = '\0';

  memcpy(&(new_genome->data_[len_ADB]), inverted_segment, len_C * sizeof(char));

  free(inverted_segment);

  // Copy segment E into the new genome
  memcpy(&(new_genome->data_[len_ADBC]), &data_[pos_E], len_E * sizeof(char));
  new_genome->data_[length_] = '\0';


  // Replace sequence
    char* old_data = data_;
    data_ = new_genome->data_;
    int new_nb_block = new_genome->nb_blocks_;
    new_genome->nb_blocks_ = nb_blocks_;

    new_genome->data_ = old_data;
    new_genome->length_ = length();

    nb_blocks_ = new_nb_block;

    release(std::move(new_genome));


  // ========== Update promoter list ==========
  if (length() >= PROM_SIZE) {
    // Remove promoters that include a breakpoint
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_B, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_C, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_D, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_E, length_);

    // Create temporary lists for promoters to move and/or invert
    PromoterList promoters_B;
    PromoterList promoters_C;
    PromoterList promoters_D;

    // 2) Extract promoters that are totally included in each segment to be
    //    moved (B, C and D)
    if (len_B >= PROM_SIZE) {
      promoters_B = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_B, pos_C);
    }
    if (len_C >= PROM_SIZE) {
      promoters_C = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_C, pos_D);
    }
    if (len_D >= PROM_SIZE) {
      promoters_D = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_D, pos_E);
    }
    // 3a) Invert promoters of segments B and C
    promoters_B.invert_promoters(pos_B, pos_C);
    promoters_C.invert_promoters(pos_C, pos_D);

    // 3b) Shift these promoters positions
    promoters_B.shift_promoters(len_D, length());
    promoters_C.shift_promoters(len_D, length());
    promoters_D.shift_promoters(-len_B - len_C, length());

    // 4) Reinsert the shifted promoters
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_C));
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_B));
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_D));

    // 5) Look for new promoters including a breakpoint
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_A);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_AD);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_ADB);
      ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_ADBC);
  }
}

void Dna::ABCDE_to_ACpDpBE(size_type pos_B, size_type pos_C, size_type pos_D, size_type pos_E) {
  // Rearrange the sequence from ABCDE to ACpDpBE (complex translocation with
  // inversion of segment defined between positions pos_C and pos_E)
  // Cp (resp Dp) stands for inverted C (resp D)
  //
  // Segments are identified by pos_x values as shown below.
  //
  // TODO(dpa) CHECK THIS !!!
  // WARNING : Segment D includes nucleotide at pos_E // NOTE : WTF???
  //
  //         A      B        C       D       E
  //      |----<]-------->=======[>=======>-------|
  //          pos_B    pos_C    pos_D   pos_E
  //
  //                         |
  //                         V
  //
  //          A       C'      D'       B       E
  //       |-----<]=======>=======<]------->-------|

  // Compute segment lengths
  auto len_A = pos_B;
  auto len_B = pos_C - pos_B;
  auto len_C = pos_D - pos_C;
  auto len_D = pos_E - pos_D;
  auto len_E = length() - pos_E;
  auto len_AC = len_A + len_C;
  auto len_ACD = len_AC + len_D;
  auto len_ACDB = len_ACD + len_B;

  // Create new sequence
  Dna* new_genome = Dna::make_dna(length_);
    new_genome->set_chrsm(ann_chrsm_);
    new_genome->reset_length(length_);

  // Copy segment A
  memcpy(new_genome->data_, data_, len_A * sizeof(char));


  // Build Cp and put it in the new genome
  char* inverted_segment;
  auto status = posix_memalign((void **)&inverted_segment,64,(len_C+1)* sizeof(char));//new char[len_C + 1];
  check_posix_memalign(status);

  for (size_type i = 0, j = pos_D - 1; i < len_C; i++, j--) {
    inverted_segment[i] = get_complementary_base(data_[j]);
  }
  inverted_segment[len_C] = '\0';

  memcpy(&(new_genome->data_[len_A]), inverted_segment, len_C * sizeof(char));

  free(inverted_segment);


  // Build Dp and put it in the new genome
  status = posix_memalign((void **)&inverted_segment,64,(len_D+1)* sizeof(char));//new char[len_D + 1];
  check_posix_memalign(status);

  for (size_type i = 0, j = pos_E - 1; i < len_D; i++, j--) {
    inverted_segment[i] = get_complementary_base(data_[j]);
  }
  inverted_segment[len_D] = '\0';

  memcpy(&(new_genome->data_[len_AC]), inverted_segment, len_D * sizeof(char));

  free(inverted_segment);

  // Copy segments B and E
  memcpy(&(new_genome->data_[len_ACD]), &data_[pos_B], len_B * sizeof(char));
  memcpy(&(new_genome->data_[len_ACDB]), &data_[pos_E], len_E * sizeof(char));
    new_genome->data_[length_] = '\0';


  // Replace sequence
    char* old_data = data_;
    data_ = new_genome->data_;

    int new_nb_block = new_genome->nb_blocks_;
    new_genome->nb_blocks_ = nb_blocks_;

    new_genome->data_ = old_data;
    new_genome->length_ = length();

    nb_blocks_ = new_nb_block;
    release(std::move(new_genome));


  // ========== Update promoter list ==========
  // 1) Remove promoters that include a breakpoint
  // 2) Extract promoters that are totally included in each segment to be
  //    moved (B, C and D)
  // 3) Shift (and invert when needed) these promoters positions
  // 4) Reinsert the shifted promoters
  // 5) Look for new promoters including a breakpoint
  if (length() >= PROM_SIZE) {
    // 1) Remove promoters that include a breakpoint
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_B, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_C, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_D, length_);
    ann_chrsm_->mutable_promoter_list().remove_promoters_around(pos_E, length_);
    // Create temporary lists for promoters to move and/or invert
    PromoterList promoters_B;
    PromoterList promoters_C;
    PromoterList promoters_D;

    // 2) Extract promoters that are totally included in each segment to be
    //    moved (B, C and D)
    if (len_B >= PROM_SIZE) {
      promoters_B = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_B, pos_C);
    }
    if (len_C >= PROM_SIZE) {
      promoters_C = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_C, pos_D);
    }
    if (len_D >= PROM_SIZE) {
      promoters_D = ann_chrsm_->mutable_promoter_list().extract_promoters_included_in(pos_D, pos_E);
    }

    // 3a) Invert promoters of segments C and D
    promoters_C.invert_promoters(pos_C, pos_D);
    promoters_D.invert_promoters(pos_D, pos_E);

    // 3b) Shift these promoters positions
    promoters_B.shift_promoters(len_C + len_D, length());
    promoters_C.shift_promoters(-len_B, length());
    promoters_D.shift_promoters(-len_B, length());

    // 4) Reinsert the shifted promoters
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_B));
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_D));
    ann_chrsm_->mutable_promoter_list().insert_promoters(std::move(promoters_C));

    // 5) Look for new promoters including a breakpoint
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_A);
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_AC);
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_ACD);
    ann_chrsm_->mutable_promoter_list().look_for_new_promoters_around(*this, len_ACDB);
  }
}
void Dna::reset_stat() {
  mutation_metrics_ = MutationMetrics();
}

/***
 * returns the subsequence of size count beginning at position first (included) on strand strand
 *
 * \param first the position of the first base (included) of the subsequence of interest
 * \param count size of the requested subsequence
 * \param strand strand of interest (note that LAGGING reads "backwards")
 *
 * \return a copy of the subsequence of interest
 */
std::string Dna::subseq(size_type first, size_type count, Strand strand) const {
  first = mod(first, length_);

  if (strand == Strand::LEADING) {
    bool spans_OriC = first + count > length_;

    if (not spans_OriC) {
      return std::string(&(data_[first]), count);
    } else {
      return std::string(&(data_[first])) + std::string(data_, count - (length_ - first));
    }
  } else { // Strand::LAGGING
    auto last = mod(first + 1 - count, length_);
    bool spans_OriC =  last >= first;

    std::string subseq;
    subseq.reserve(count);

    if (not spans_OriC) {
      std::transform(std::make_reverse_iterator(&(data_[first + 1])),
                     std::make_reverse_iterator(&(data_[last])),
                     std::back_inserter(subseq),
                     get_complementary_base);
      return subseq;
    } else { // spans_OriC
      std::transform(std::make_reverse_iterator(&(data_[first + 1])),
                     std::make_reverse_iterator(&(data_[0])),
                     std::back_inserter(subseq),
                     get_complementary_base);
      auto remains_to_transform = count - subseq.size();
      std::transform(std::make_reverse_iterator(&(data_[length_])),
                     std::make_reverse_iterator(&(data_[length_ - remains_to_transform])),
                     std::back_inserter(subseq),
                     get_complementary_base);
      return subseq;
    }
  }
}

std::ostream& operator<<(std::ostream& os, const Dna& o) {
  #if BASE_2
  os << o.data();
  #else
  // Transform to ACGT
  auto r = std::ranges::transform_view(std::ranges::subrange(o.data_, o.data_ + o.length_), num_to_atcg);
  os << std::string(r.begin(), r.end());
  #endif

  return os;
}

}
