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

#include "Individual.h"
#include "Population.h"

#include <list>
#include <vector>
#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

namespace aevol {

// Static instance
std::unique_ptr<DnaFactory> dna_factory = std::make_unique<DnaFactory>(DnaFactory_Policy::ALLOCATE, 0, 0);

DnaFactory::DnaFactory(DnaFactory_Policy policy, int pool_size, Dna::size_type init_size, int pop_size) {
  policy_    = policy;
  pool_size_ = pool_size;
  init(init_size, pop_size);
}

DnaFactory::~DnaFactory() {
  for (auto &&it_dna: list_unused_dna_) {
    delete it_dna;
  }
  list_unused_dna_.clear();

  for (size_t j = 0; j < local_list_unused_dna_.size(); j++) {
    for (auto &&it_dna: local_list_unused_dna_[j]) {
      delete it_dna;
    }
    local_list_unused_dna_[j].clear();
  }
  local_list_unused_dna_.clear();
}

void DnaFactory::init(Dna::size_type init_size, int pop_size) {
  if (policy_ != DnaFactory_Policy::LOCAL_GLOBAL_FIT) {
    if (policy_ != DnaFactory_Policy::ALLOCATE) {
      for (int i = 0; i < pool_size_; i++) {
        list_unused_dna_.push_back(new Dna(init_size));
      }
    }
  } else {
    int n = 0;
    #pragma omp parallel reduction(+ : n)
    { n += 1; }

    local_list_unused_dna_.resize(n);
    if (pop_size != -1) {
      local_pool_size_  = std::max((pop_size / n) * 4, 1);
      global_pool_size_ = pop_size * 2;
    }

    for (int j = 0; j < n; j++) {
      for (decltype(local_pool_size_) i = 0; i < local_pool_size_; i++) {
        local_list_unused_dna_[j].push_back(new Dna(init_size));
      }
    }

    for (int i = 0; i < global_pool_size_; i++) {
      list_unused_dna_.push_back(new Dna(init_size));
    }
  }
}

Dna* DnaFactory::get_dna(Dna::size_type request_size) {
  auto req_block = Dna::nb_required_blocks(request_size);
  if (policy_ == DnaFactory_Policy::FIRST) {
    Dna* pop      = nullptr;
    bool allocate = false;
    #pragma omp critical(pop_dna)
    {
      if (list_unused_dna_.empty()) {
        allocate = true;
      } else {
        pop = list_unused_dna_.front();
        pop->reset_length(request_size);
        list_unused_dna_.pop_front();
      }
    }
    if (allocate)
      pop = new Dna(request_size);

    return pop;
  } else if (policy_ == DnaFactory_Policy::FIRSTFIT) {
    Dna* pop = nullptr;

    #pragma omp critical(pop_dna)
    {
      if (list_unused_dna_.empty()) {
        pop = new Dna(request_size);
      } else {
        std::list<Dna*>::iterator found_it;
        for (auto it = list_unused_dna_.begin(); it != list_unused_dna_.end(); it++) {
          if ((*it)->nb_block() >= req_block) {
            found_it = it;
            pop      = (*it);
            pop->reset_length(request_size);
            break;
          }
        }

        if (pop == nullptr) {
          pop = list_unused_dna_.front();
          pop->reset_length(request_size);
          list_unused_dna_.pop_front();
        } else {
          list_unused_dna_.erase(found_it);
        }
      }
    }
    return pop;
  } else if (policy_ == DnaFactory_Policy::LOCAL_GLOBAL_FIT) {
    Dna* pop = nullptr;

    if (local_list_unused_dna_[omp_get_thread_num()].empty()) {
      if (list_unused_dna_.empty()) {
        pop = new Dna(request_size);
      } else {
        // Go to global (and lock)
        #pragma omp critical(pop_dna)
        {
          std::list<Dna*>::iterator found_it;
          for (auto it = list_unused_dna_.begin(); it != list_unused_dna_.end(); it++) {
            if ((*it)->nb_block() >= req_block) {
              found_it = it;
              pop      = (*it);
              pop->reset_length(request_size);
              break;
            }
          }

          if (pop == nullptr) {
            pop = list_unused_dna_.front();
            pop->reset_length(request_size);
            list_unused_dna_.pop_front();
          } else {
            list_unused_dna_.erase(found_it);
          }
        }
      }
    } else {
      // Go to local (and lock free)
      std::list<Dna*>::iterator found_it;

      for (auto it = local_list_unused_dna_[omp_get_thread_num()].begin();
           it != local_list_unused_dna_[omp_get_thread_num()].end();
           it++) {
        if ((*it)->nb_block() >= req_block) {
          found_it = it;
          pop      = (*it);
          pop->reset_length(request_size);
          break;
        }
      }

      if (pop == nullptr) {
        pop = local_list_unused_dna_[omp_get_thread_num()].front();
        pop->reset_length(request_size);
        local_list_unused_dna_[omp_get_thread_num()].pop_front();
      } else {
        local_list_unused_dna_[omp_get_thread_num()].erase(found_it);
      }
    }
    return pop;
  } else if (policy_ == DnaFactory_Policy::ALLOCATE) {
    return new Dna(request_size);
  }
  return nullptr;
}

void DnaFactory::give_back(Dna* dna) {
  dna->reset_stat();
  if (policy_ == DnaFactory_Policy::LOCAL_GLOBAL_FIT) {
    if (local_list_unused_dna_[omp_get_thread_num()].size() == local_pool_size_) {
      Dna* pop = local_list_unused_dna_[omp_get_thread_num()].back();
      local_list_unused_dna_[omp_get_thread_num()].pop_back();

      #pragma omp critical(pop_dna)
      { list_unused_dna_.push_back(pop); }
    }

    local_list_unused_dna_[omp_get_thread_num()].push_front(dna);
  } else if (policy_ == DnaFactory_Policy::ALLOCATE) {
    delete dna;
  } else {
    #pragma omp critical(pop_dna)
    { list_unused_dna_.push_back(dna); }
  }
}

void DnaFactory::reduce_space(const Population& population) {
  if (garbage_policy == DnaFactory_Garbage_Policy::MAXSIZE) {
    Dna::size_type max_size = 0;

    for (indiv_id_type indiv_id = 0; indiv_id < population.size(); ++indiv_id) {
      max_size = population[indiv_id].dna().length() > max_size
                     ? population[indiv_id].dna().length()
                     : max_size;
      if (exp_setup->diploid()) {
        max_size = population[indiv_id].dna(Chrsm::B).length() > max_size
                       ? population[indiv_id].dna(Chrsm::B).length()
                       : max_size;
      }
    }

    for (decltype(local_list_unused_dna_.size()) j = 0; j < local_list_unused_dna_.size(); j++) {
      for (std::list<Dna*>::iterator it_dna = local_list_unused_dna_[j].begin();
           it_dna != local_list_unused_dna_[j].end();) {
        bool toRemove  = false;
        auto it_remove = it_dna;
        if ((*it_dna)->length_ > max_size) {
          toRemove = true;
        }
        it_dna++;
        if (toRemove) {
          delete (*it_remove);
          local_list_unused_dna_[j].erase(it_remove);
        }
      }
    }

    for (std::list<Dna*>::iterator it_dna = list_unused_dna_.begin(); it_dna != list_unused_dna_.end();) {
      bool toRemove  = false;
      auto it_remove = it_dna;
      if ((*it_dna)->length_ > max_size) {
        toRemove = true;
      }

      it_dna++;

      if (toRemove) {
        delete (*it_remove);
        list_unused_dna_.erase(it_remove);
      }
    }
  } else if (garbage_policy == DnaFactory_Garbage_Policy::MAXMEM) {
    size_t total_length_    = 0;
    size_t *a_total_length_ = new size_t[local_list_unused_dna_.size()];
    for (std::list<Dna*>::iterator it_dna = list_unused_dna_.begin(); it_dna != list_unused_dna_.end(); it_dna++) {
      total_length_ += (*it_dna)->nb_block() * BLOCK_SIZE * sizeof(char);
    }

    for (size_t j = 0; j < local_list_unused_dna_.size(); j++) {
      a_total_length_[j] = 0;
      for (std::list<Dna*>::iterator it_dna = local_list_unused_dna_[j].begin();
           it_dna != local_list_unused_dna_[j].end();
           it_dna++) {
        total_length_ += (*it_dna)->nb_block() * BLOCK_SIZE * sizeof(char);
        a_total_length_[j] += (*it_dna)->nb_block() * BLOCK_SIZE * sizeof(char);
      }

      a_total_length_[j] /= 1000000;
    }

    total_length_ /= 1000000;

    if (total_length_ > max_pool_size) {
      size_t to_remove_size = total_length_ - max_pool_size;

      list_unused_dna_.sort([](Dna* a, Dna* b) { return a->length_ > b->length_; });

      for (std::list<Dna*>::iterator it_dna = list_unused_dna_.begin(); it_dna != list_unused_dna_.end();) {
        auto it_remove = it_dna;
        it_dna++;

        to_remove_size -= ((*it_remove)->length_ / 1000000);

        delete (*it_remove);
        list_unused_dna_.erase(it_remove);
      }

      if (to_remove_size > 0) {
        std::vector<std::list<Dna*>::iterator> list_iter(local_list_unused_dna_.size());

        for (decltype(local_list_unused_dna_.size()) j = 0; j < local_list_unused_dna_.size(); j++) {
          local_list_unused_dna_[j].sort([](Dna* a, Dna* b) { return a->length_ > b->length_; });
          list_iter[j] = local_list_unused_dna_[j].begin();
        }

        while (to_remove_size > 0) {

          for (decltype(local_list_unused_dna_.size()) j = 0; j < local_list_unused_dna_.size(); j++) {
            if (list_iter[j] != local_list_unused_dna_[j].end()) {
              auto it_remove = list_iter[j];
              list_iter[j]++;

              to_remove_size -= ((*it_remove)->length_ / 1000000);

              delete (*it_remove);
              local_list_unused_dna_[j].erase(it_remove);
            }
          }
        }
      }
    }

    delete[] a_total_length_;
  }
}

}  // namespace aevol
