#' @title Merge function for RLum.Data.Curve S4 class objects
#'
#' @description This function allows to merge [Luminescence::RLum.Data.Curve-class] objects in different
#' ways without modifying the original objects. Merging is always applied on
#' the 2nd column of the object's data matrix.
#'
#' **Supported merge operations are:**
#'
#' `"mean"` (default)
#'
#' The mean over the count values is calculated using the function
#' [rowMeans].
#'
#' `"median"`
#'
#' The median over the count values is calculated using the function
#' [matrixStats::rowMedians].
#'
#' `"sum"`
#'
#' All count values will be summed up using the function [rowSums].
#'
#' `"sd"`
#'
#' The standard deviation over the count values is calculated using the function
#' [matrixStats::rowSds].
#'
#' `"var"`
#'
#' The variance over the count values is calculated using the function
#' [matrixStats::rowVars].
#'
#' `"min"`
#'
#' The min values from the count values is chosen using the function
#' [matrixStats::rowMins][matrixStats::rowRanges].
#'
#' `"max"`
#'
#' The max values from the count values is chosen using the function
#' [matrixStats::rowMins][matrixStats::rowRanges].
#'
#' `"append"`
#'
#' Appends count values of all curves to one combined data curve. The channel width
#' is automatically re-calculated, but requires a constant channel width of the
#' original data.
#'
#' `"-"`
#'
#' The row sums of the last objects are subtracted from the first object.
#'
#' `"*"`
#'
#' The row sums of the last objects are multiplied with the first object.
#'
#' `"/"`
#'
#' Values of the first object are divided by row sums of the last objects.
#'
#' @param object [list] of [Luminescence::RLum.Data.Curve-class] (**required**):
#' list of objects to be merged.
#'
#' @param merge.method [character] (**required**):
#' method for combining of the objects, e.g. `'mean'` (default), `'median'`,
#' `'sum'`, see details for
#' further information and allowed methods.  Note: Elements in slot info will
#' be taken from the first curve in the list.
#'
#' @param method.info [numeric] (*optional*):
#' allows to specify how info elements of the input objects are combined,
#' e.g. `1` means that just the elements from the first object are kept,
#' `2` keeps only the info elements from the 2 object etc.
#' If set to `NULL`, all elements are combined.
#'
#' @return Returns an [Luminescence::RLum.Data.Curve-class] object.
#'
#' @note
#' The information from the slot `recordType` is taken from the first
#' object in the input list. The slot
#' 'curveType' is filled with the name `merged`.
#'
#' @section S3-generic support:
#'
#' This function is fully operational via S3-generics:
#' ``+``, ``-``, ``/``, ``*``, `merge`
#'
#' @section Function version: 0.2.2
#'
#' @author
#' Sebastian Kreutzer, F2.1 Geophysical Parametrisation/Regionalisation, LIAG - Institute for Applied Geophysics (Germany)
#'
#' @seealso [Luminescence::merge_RLum], [Luminescence::RLum.Data.Curve-class]
#'
#' @keywords utilities internal
#'
#' @examples
#'
#' ##load example data
#' data(ExampleData.XSYG, envir = environment())
#'
#' ##grep first and 3d TL curves
#' TL.curves  <- get_RLum(OSL.SARMeasurement$Sequence.Object, recordType = "TL (UVVIS)")
#' TL.curve.1 <- TL.curves[[1]]
#' TL.curve.3 <- TL.curves[[3]]
#'
#' ##plot single curves
#' plot_RLum(TL.curve.1)
#' plot_RLum(TL.curve.3)
#'
#' ##subtract the 1st curve from the 2nd and plot
#' TL.curve.merged <- merge_RLum.Data.Curve(list(TL.curve.3, TL.curve.1), merge.method = "/")
#' plot_RLum(TL.curve.merged)
#'
#' @export
merge_RLum.Data.Curve<- function(
  object,
  merge.method = c("mean", "median", "sum", "sd", "var", "max", "min",
                   "append", "-", "*", "/"),
  method.info = NULL
) {
  .set_function_name("merge_RLum.Data.Curve")
  on.exit(.unset_function_name(), add = TRUE)

  ## Integrity checks -------------------------------------------------------
  .validate_class(object, "list")
  .validate_positive_scalar(method.info, int = TRUE, null.ok = TRUE)

  ##(1) check if object is of class RLum.Data.Curve
  temp.recordType.test <- sapply(object, function(x) {
    .validate_class(x, "RLum.Data.Curve",
                    name = "All elements of 'object'")
    return(x@recordType)
  })

  ##(2) Check for similar record types
  record.types <- unique(temp.recordType.test)
  if (length(record.types) > 1) {
    .throw_error("Objects cannot be merged, different record types found: ",
                 .collapse(record.types))
  }

  merge.method <- .validate_args(merge.method,
                                 c("mean", "median", "sum", "sd", "var", "max",
                                   "min", "append", "-", "*", "/"))

  ## Merge objects ----------------------------------------------------------
  ##merge data objects
  ##problem ... how to handle data with different resolution or length?

  ##(1) build new data matrix
  ## first find the shortest object
  check.rows <- vapply(object, function(x) nrow(x@data), numeric(1))
  if (length(check.rows) < 1 || min(check.rows) < 2) {
    .throw_error("'object' contains no data")
  }
  num.rows <- min(check.rows)

  ## channel resolution of the first object: we need to round as there may
  ## otherwise be numerical artefacts that would make the step not unique
  step <- round(diff(object[[1]]@data[, 1]), 1)[1]

  ## extract the curve values from each object
  temp.matrix <- sapply(object, function(x) {
    ## check the resolution (roughly)
    if (round(diff(x@data[, 1]), 1)[1] != step)
      .throw_warning("The curves do not seem to have the same channel resolution")
    ## limit all objects to the shortest one
    x@data[1:num.rows, 2]
  })

  ## throw the warning only now to avoid printing it in case of error
  if (length(unique(check.rows)) != 1) {
    .throw_warning("The number of channels differs between the curves, the ",
                   "merged curve will have the length of the shortest object")
  }

  ##(2) apply selected method for merging
  temp.matrix <- switch(merge.method,
                        sum = rowSums(temp.matrix),
                        mean = rowMeans(temp.matrix),
                        median = matrixStats::rowMedians(temp.matrix),
                        sd = matrixStats::rowSds(temp.matrix),
                        var = matrixStats::rowVars(temp.matrix),
                        max = matrixStats::rowMaxs(temp.matrix),
                        min = matrixStats::rowMins(temp.matrix),
                        append = sapply(temp.matrix, c),
                        "-" = {
                          temp.matrix[, 1] - rowSums(temp.matrix[, -1, drop = FALSE])
                        },
                        "*" = {
                          temp.matrix[, 1] * rowSums(temp.matrix[, -1, drop = FALSE])
                        },
                        "/" = {
                          temp <- temp.matrix[, 1] / rowSums(temp.matrix[, -1, drop = FALSE])

                          ## replace infinities with 0 and throw warning
                          id.inf <- which(is.infinite(temp))
                          if (length(id.inf) > 0) {
                            temp[id.inf]  <- 0
                            .throw_warning(length(id.inf),
                                           " 'Inf' values replaced by 0 in the matrix")
                          }
                          temp
                        })

  ## add back the first column to RLum.Data.Curve objects
  #If we append the data of the second to the first curve we have to recalculate
  #the x-values (probably time/channel). The difference should always be the
  #same, so we just expand the sequence if this is true. If this is not true,
  #we revert to the default behaviour (i.e., append the x values)
  if (merge.method == "append") {
    newx <- seq(from = min(object[[1]]@data[, 1]), by = step,
                length.out = sum(check.rows))
    temp.matrix <- cbind(newx, temp.matrix)
  } else {
    temp.matrix <- cbind(object[[1]]@data[1:num.rows, 1], temp.matrix)
  }

  ## remove spurious column names added by cbind()
  temp.matrix <- unname(temp.matrix)

  ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ##merge info objects as simple as possible ... just keep them all ... other possibility
  ##would be to choose on the input objects

  ##unlist is needed here, as otherwise it would cause unexpected behaviour further using
  ##the RLum.object
  if (is.null(method.info)) {
    temp.info <- unlist(lapply(object, function(x) x@info), recursive = FALSE)
  }else{
    temp.info <- object[[method.info]]@info
  }

  ## Build new RLum.Data.Curve object ---------------------------------------
  set_RLum(
    class = "RLum.Data.Curve",
    originator = "merge_RLum.Data.Curve",
    recordType = object[[1]]@recordType,
    curveType =  "merged",
    data = temp.matrix,
    info = temp.info,
    .pid = unlist(lapply(object, function(x) x@.uid))
  )
}
