#' Mating
#' @description Mating (selection) method (algorithm) to be used in the
#' genetic algorithm.
#' @param population numeric matrix whose rows are chromosomes, i.e., vectors of 
#' parameter values.
#' @param fitness numeric vector whose \code{i}-th element is the value of 
#' \code{fn} at point \code{population[i, ]}.
#' @param parents.n even positive integer representing the number of parents.
#' @param method mating method to be used for selection of parents.
#' @param par additional parameters to be passed depending on the \code{method}.
#' @param self logical; if \code{TRUE} then a chromosome may mate with itself. 
#' Otherwise, mating is allowed only between different chromosomes.
#' @param iter iteration number of the genetic algorithm.
#' @details Denote \code{population} by \eqn{C} whose \code{i}-th row 
#' \code{population[i, ]} is a chromosome \eqn{c_{i}}, i.e., the vector of 
#' parameter values of the function being optimized \eqn{f(.)} that is
#' provided via the \code{fn} argument of \code{\link[gena]{gena}}.
#' The elements of chromosome \eqn{c_{ij}} are genes representing parameters 
#' values. Argument \code{fitness} is a vector of function values at
#' corresponding chromosomes i.e. \code{fitness[i]} corresponds to
#' \eqn{f_{i}=f(c_{i})}. Total number of chromosomes in population
#' \eqn{n_{population}} equals to \code{nrow(population)}.
#' 
#' The mating algorithm determines the selection of chromosomes that will become 
#' parents. During each iteration of mating, one of the chromosomes becomes a 
#' parent until there are \eqn{n_{parents}} (i.e., \code{parents.n}) parents 
#' selected. Each chromosome may become a parent multiple times or not become a 
#' parent at all.
#' 
#' Denote by \eqn{c^{s}_{i}} the \eqn{i}-th selected parent. Parents
#' \eqn{c^{s}_{i}} and \eqn{c^{s}_{i + 1}} form a pair that will further
#' produce a child (offspring), where \eqn{i} is odd.
#' If \code{self = FALSE}, then for each pair of parents 
#' \eqn{(c_{i}^s, c_{i+1}^s)} it is ensured that 
#' \eqn{c^{s}_{i} \ne c^{s}_{i + 1}} except in the case when there are several 
#' identical chromosomes in the population. However, \code{self} is ignored 
#' if \code{method} is \code{"tournament"}, so in this case self-mating
#' is always possible.
#' 
#' Denote by \eqn{p_{i}} the probability of a chromosome becoming a parent.
#' Note that each chromosome may become a parent multiple times.
#' The probability \eqn{p_{i}\left(f_{i}\right)} is a function 
#' of fitness \eqn{f_{i}}. Usually this function is non-decreasing, so 
#' fitter chromosomes have a higher probability of becoming a parent. 
#' There is also an intermediate value \eqn{w_{i}} called the weight such that:
#' \deqn{p_{i}=\frac{w_{i}}{\sum\limits_{j=1}^{n_{population}}w_{j}}}
#' Therefore, all weights \eqn{w_{i}} are proportional to the corresponding 
#' probabilities \eqn{p_{i}} by the same factor (the sum of weights).
#' 
#' Argument \code{method} determines particular mating algorithm to be applied.
#' Denote by \eqn{\tau} the vector of parameters used by the algorithm.
#' Note that \eqn{\tau} corresponds to \code{par}. The algorithm determines
#' a particular form of the \eqn{w_{i}\left(f_{i}\right)} function which 
#' in turn determines \eqn{p_{i}\left(f_{i}\right)}.
#' 
#' If \code{method = "constant"}, then all weights and probabilities are equal:
#' \deqn{w_{i}=1 => p_{i}=\frac{1}{n_{population}}}
#' 
#' If \code{method = "rank"}, then each chromosome receives a rank \eqn{r_{i}} 
#' based on the fitness \eqn{f_{i}} value. So if \code{j}-th chromosome is the
#' fittest one and \code{k}-th chromosome has the lowest fitness value, then
#' \eqn{r_{j}=n_{population}} and  \eqn{r_{k}=1}. The relationship
#' between weight \eqn{w_{i}} and rank \eqn{r_{i}} is as follows:
#' \deqn{w_{i}=\left(\frac{r_{i}}{n_{population}}\right)^{\tau_{1}}}
#' The greater value of \eqn{\tau_{1}} the greater portion of probability will
#' be delivered to fitter chromosomes. 
#' Default value is \eqn{\tau_{1} = 0.5} so \code{par = 0.5}.
#' 
#' If \code{method = "fitness"} then weights are calculated as follows:
#' \deqn{w_{i}=\left(f_{i} - 
#'                   \min\left(f_{1},...,f_{n_{population}}\right) + 
#'                   \tau_{1}\right)^{\tau_{2}}}
#' By default \eqn{\tau_{1}=10} and \eqn{\tau_{2}=0.5}, i.e., 
#' \code{par = c(10, 0.5)}. There is a restriction \eqn{\tau_{1}\geq0}
#' ensuring that the expression in brackets is non-negative.
#' 
#' If \code{method = "tournament"} then  \eqn{\tau_{1}} (i.e., \code{par}) 
#' chromosomes will be randomly selected with equal probabilities and without 
#' replacement. Then the chromosome with the highest fitness 
#' (among these selected chromosomes) will become a parent.
#' It is possible to provide the representation of this algorithm via 
#' probabilities \eqn{p_{i}} but the formulas are numerically unstable.
#' By default \code{par = min(5, ceiling(parents.n * 0.1))}.
#' 
#' Validation and default values assignment for \code{par} are performed inside
#' the \code{\link[gena]{gena}} function not in \code{\link[gena]{gena.mating}}.
#' This allows validation to be performed a single time instead of repeating it
#' each iteration of genetic algorithm.
#' 
#' For more information on mating (selection) algorithms,
#' please see Shukla et al. (2015).
#' 
#' @return The function returns a list with the following elements:
#' \itemize{
#' \item \code{parents} - matrix whose rows are parents. The number of
#' rows of this matrix equals to \code{parents.n} while the number of columns
#' is \code{ncol(population)}.
#' \item \code{fitness} - vector whose i-th element is the fitness of the
#' i-th parent.
#' \item \code{ind} - vector whose i-th element is the index of the i-th
#' parent in the population, so \code{$parents[i, ]} equals 
#' \code{population[ind[i], ]}.
#' }
#' @references A. Shukla, H. Pandey, D. Mehrotra (2015). 
#' Comparative review of selection techniques in genetic algorithm.
#' \emph{2015 International Conference on Futuristic Trends on Computational 
#' Analysis and Knowledge Management (ABLAZE)}, 515-519,
#' <doi:10.1109/ABLAZE.2015.7154916>.
#' @examples 
#' # Consider the following fitness function
#' fn <- function(x)
#' {
#'   val <- x[1] * x[2] - x[1] ^ 2 - x[2] ^ 2
#' }
#' 
#' # Randomly initialize the population
#' set.seed(123)
#' n.pop <- 10
#' population <- gena.population(pop.n = n.pop,
#'                               lower = c(-5, -5), 
#'                               upper = c(5, 5))
#'
#' # Calculate fitness of each chromosome
#' fitness <- rep(NA, n.pop)
#' for(i in 1:n.pop)
#' {
#'   fitness[i] <- fn(population[i, ])
#' }
#' 
#' # Perform mating to select parents
#' parents <- gena.mating(population = population,
#'                        fitness = fitness,
#'                        parents.n = n.pop,
#'                        method = "rank",
#'                        par = 0.8)
#' print(parents)
#' 
gena.mating <- function(population,                        # population of chromosomes
                        fitness,                           # fitness values of chromosomes
                        parents.n,                         # number of parents to select
                        method = "rank",                   # algorithm to assign probabilities 
                                                           # of being a parent
                        par = NULL,                        # additional parameters
                                                           # depending on "method"
                        self = FALSE,
                        iter = NULL)                       # genetic algorithm iteration              
{      
  # ---
  # The algorithm brief description:
  # 1. For each chromosome (solution) estimate the 
  #    probability of being selected to be a parent.
  # 2. Select the parents according to estimated
  #    probabilities or some other criteria.
  # 3. Return the parents along with their fitness.
  # ---
  
  # Prepare some values
  
  pop.n <- nrow(population)                                # the number of chromosomes
  ind_pop <- 1:pop.n                                       # chromosomes indexes
  
  # Estimate the probability of being a parent
  # for each chromosome
  
  probs <- rep(1 / pop.n, pop.n)                           # default probabilities vector
  
  if (method == "rank")                                    # rank-based probabilities
  {
    fitness_rank <- rank(fitness)                          # rank by fitness
    fitness_weight <- (fitness_rank / pop.n) ^ par[1]      # weight according to rank
    probs <- fitness_weight / sum(fitness_weight)          # assign probabilities
                                                           # proportional to weights
  }
  
  if (method == "fitness")                                 # fitness-based probabilities
  {
    fitness_weight <- (fitness - min(fitness) + par[1]) ^  # weight according 
                      par[2]                               # to the fitness
    probs <- fitness_weight / sum(fitness_weight)          # assign probabilities
  }                                                        # proportional to weights
  
  # Select the parents based on probabilities
  
  parents_ind <- rep(NA, parents.n)                        # vector to store
                                                           # parent indices
  
  if (method %in% c("constant", "rank", "fitness"))
  {
    parents_ind <- sample(ind_pop,                         # indexes of parents
                          size = parents.n,            
                          replace = TRUE, 
                          prob = probs)
  }
  
  # Select parents following some other criteria
  
  if (method == "tournament")                              # select parents 
  {                                                        # via the tournament
    pop_ind <- 1:pop.n
    parents_ind <- mating_tournament(                      # Rcpp version for
      pop_ind = pop_ind, candidates_n = par[1],            # speed improvement
      fitness = fitness, parents_n = parents.n)                                 
    # for(i in 1:parents.n)
    # {
    #   candidates <- sample(pop_ind,                      # randomly pick
    #                        size = par[1],                # some candidates
    #                        replace = FALSE)
    #   parents_ind[i] <- candidates[                      # select the fittest
    #     which.max(fitness[candidates])]                  # of all candidates
    # }
  }
    # ---
    # Comment to tournament method:
    # It is possible to assign the probabilities
    # but it requires combinatorial formulas to be
    # used that may cause numeric instability
    # ---
  
  # Prevent self-mating
  
  odd_ind <- NULL
  
  if (!self)
  {
    odd_ind <- seq(from = 1, to = parents.n, by = 2)
    if (method %in% c("constant", "rank", "fitness"))
    {
      for (i in odd_ind)
      {
        if (parents_ind[i] == parents_ind[i + 1])
        {
          parents_ind[i] <- sample(ind_pop[-parents_ind[i]],
                                   size = 1,            
                                   replace = TRUE, 
                                   prob = probs[-parents_ind[i]] / 
                                          sum(probs[-parents_ind[i]]))
        }
      }
    }
  }
  
  # Save and return the results of selection
  
  parents <- population[parents_ind, , drop = FALSE]       # parents and
  parents_fitness <- fitness[parents_ind]                  # their fitness
  
  return_val <- list(parents = parents,                    # output in the form
                     fitness = parents_fitness,            # of a list
                     ind = parents_ind)            
  
  return(return_val)                                       # return the parents
                                                           # along with their 
                                                           # fitness
}

# Assign default parameters for
# a mating algorithm depending
# on the "method"
gena.mating.validate <- function(method, par, parents.n)
{
  # Validate the "method"
  
  methods <- c("constant", "rank", "fitness",              # the list of all
               "tournament")                               # available methods
                                                           
  if (!(method %in% methods))
  {
    stop(paste0("Incorrect mating.method argument. ",      # if the user has provided
                "It should be one of: ",                   # incorrect argument
                paste(methods, collapse = ", "),
                "\n"))
  }
  
  # Assign default parameters
  
  if (method == "rank")
  {
    if (!is.null(par))
    {
      if ((length(par) != 1) | (!is.numeric(par)))
      {
        stop(paste0("Incorrect mating.par argument. Please, ensure that ",
                    "(length(mating.par) == 1) and is.numeric(mating.par)",
                    "\n"))
      }
    } else {
      par <- 0.5
    }
  }
  
  if (method == "fitness")
  {
    if (!is.null(par))
    {
      if ((length(par) != 2) | (par[1] < 0) | (!is.numeric(par)))
      {
        stop(paste0("Incorrect mating.par argument. Please, ensure that ",
                    "(length(mating.par) == 2), (mating.par[1] > 0) ",
                    "and is.numeric(mating.par)",
                    "\n"))
      }
    } else {
      par <- c(10, 0.5)
    }
  }
  
  if (method == "tournament")
  {
    if (!is.null(par))
    {
      if ((length(par) != 1) | (par < 1) | (par > parents.n) | 
         (!is.numeric(par)))
      {
        stop(paste0("Incorrect mating.par argument. Please, ensure that ",
                    "(length(mating.par) == 1), (mating.par >= 1)",
                    "(mating.par < (pop.n - n_elite)), and ",
                    "is.numeric(mating.par)",
                    "\n"))
      }
    } else {
      par <- min(5, ceiling(parents.n * 0.1))
    }
  }
  
  return(par)
}