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

#include "rng/JumpingMT.h"

namespace aevol {

namespace align {

auto compute_score(std::array<std::pair<const Dna*, Dna::size_type>, 2> alignment,
                   bool forward,
                   score_t match_bonus,
                   score_t mismatch_cost,
                   score_t target_score,
                   Dna::size_type max_align_size) -> score_t;

auto generate_random_position_and_counterpart(std::array<Dna::size_type, 2> chrsm_sizes,
                                              JumpingMT& prng,
                                              double slippage_sigma) -> std::tuple<Dna::size_type, Dna::size_type>;

auto RecombAlignmentSearcher::find_alignment(std::array<const Dna*, 2> chrsms,
                                             JumpingMT& prng,
                                             const AlignmentParams& params) -> Alignment {
  auto best_align = Alignment{0, 0, -1};
  auto max_tries  = size_t(std::lround(params.neighbourhood_rate() * (chrsms[0]->length() + chrsms[1]->length())));
  auto nb_tries   = decltype(max_tries){0};
  score_t target_score = params.generate_target_score(prng);

  // 2 random positions will be generated that must be approximately "physically aligned".
  // The first position will be generated using a uniform distribution on the corresponding chrsm length.
  // The second position must be approximately "physically aligned" to its counterpart, i.e. if posA sits after 10%
  // of genomeA, posB must sit after ~10% of genomeB.
  // This variable is the sigma parameter of the gaussian distribution used to generate an *approximate* counterpart
  auto slippage_sigma = 0.05;

  while(++nb_tries <= max_tries and best_align.score < target_score) {
    auto align = Alignment{};
    std::tie(align.posA, align.posB) =
        generate_random_position_and_counterpart({chrsms[0]->length(), chrsms[1]->length()}, prng, slippage_sigma);
    align.score = compute_score({{{chrsms[0], align.posA}, {chrsms[1], align.posB}}},
                                (prng.random() < 0.5),
                                params.match_bonus(),
                                params.mismatch_cost(),
                                target_score,
                                params.max_align_size());
    if (align.score > best_align.score) {
      best_align = align;
    }
  }

  return best_align;
}

auto compute_score(std::array<std::pair<const Dna*, Dna::size_type>, 2> alignment,
                   bool forward,
                   score_t match_bonus,
                   score_t mismatch_cost,
                   score_t target_score,
                   Dna::size_type max_align_size) -> score_t {
  auto align_size = Dna::size_type{0};
  auto max_score = score_t{0};
  auto cur_score = score_t{0};
  const auto& chrsm_0 = *alignment[0].first;
  const auto& chrsm_1 = *alignment[1].first;
  auto cur_pos_0 = alignment[0].second;
  auto cur_pos_1 = alignment[1].second;

  while (true) {
    // Try match at current positions
    ++align_size;
    if (chrsm_0[cur_pos_0] == chrsm_1[cur_pos_1]) {
      cur_score += match_bonus;
      max_score = cur_score > max_score ? cur_score : max_score;
    } else {
      cur_score += mismatch_cost;
    }

    // If we've reached the target score, return current score
    if (cur_score >= target_score) return cur_score;

    // If we've reached the maximum alignment size, return max_score
    if (align_size >= max_align_size) return max_score;

    // If the score has fallen below 0, return max_score
    if (cur_score < 0) return max_score;

    // Go forward (backward)
    if (forward) {
      ++cur_pos_0;
      ++cur_pos_1;
    } else {
      --cur_pos_0;
      --cur_pos_1;
    }

    // If we're past the end of one of the chromosomes
    if (cur_pos_0 < 0 or cur_pos_1 < 0 or cur_pos_0 >= chrsm_0.length() or cur_pos_1 >= chrsm_1.length()) {
      return max_score;
    }
  }
}

auto generate_random_position_and_counterpart(std::array<Dna::size_type, 2> chrsm_sizes,
                                              JumpingMT& prng,
                                              double slippage_sigma) -> std::tuple<Dna::size_type, Dna::size_type> {
  auto chrsm_size_ratio = (chrsm_sizes[1] - 1.) / (chrsm_sizes[0] - 1.);

  while (true) {
    auto pos0 = Dna::size_type{prng.random(chrsm_sizes[0])};
    auto pos1 =
        Dna::size_type(std::lround(prng.gaussian_random(pos0 * chrsm_size_ratio, slippage_sigma * chrsm_sizes[1])));

    if (pos1 >= 0 and pos1 < chrsm_sizes[1])
      return {pos0, pos1};
  }
}

}  // namespace align

}  // namespace aevol
