#' Stack time series data
#'
#' @description
#'
#' \if{html,text}{(\emph{version française: 
#' \url{https://StatCan.github.io/gensol-gseries/fr/reference/stack_tsDF.html}})}
#' 
#' Convert a multivariate time series data frame (see [ts_to_tsDF()]) for the benchmarking functions
#' ([benchmarking()] and [stock_benchmarking()]) into a stacked (tall) data frame with four variables (columns):
#' * one (1) for the series name
#' * two (2) for the data point identification (year and period)
#' * one (1) for the data point value
#'
#' Missing (`NA`) series values are not included in the output stacked data frame by default. Specify argument 
#' `keep_NA = TRUE` in order to keep them.
#'
#' This function is useful when intending to use the `by` argument (*BY-group* processing mode) of the benchmarking
#' functions in order to benchmark multiple series in a single function call.
#'
#'
#' @param ts_df (mandatory)
#'
#' Data frame (object of class "data.frame") that contains the multivariate time series data to be stacked.
#'
#' @param ser_cName (optional)
#'
#' String specifying the name of the character variable (column) in the output stacked data frame that will
#' contain the series names (name of the time series variables in the input multivariate time series data
#' frame). This variable can then be used as the BY-group variable (argument `by`) with the benchmarking functions.
#'
#' **Default value** is `ser_cName = "series"`.
#'
#' @param yr_cName,per_cName (optional)
#'
#' Strings specifying the name of the numeric variables (columns) in the input multivariate time series data frame
#' that contain the data point year and period (cycle) identifiers. These variables are *transferred* to the output 
#' stacked data frame with the same variable names.
#'
#' **Default values** are `yr_cName = "year"` and `per_cName   = "period"`.
#'
#' @param val_cName (optional)
#'
#' String specifying the name of the numeric variable (column) in the output stacked data frame that will
#' contain the data point values.
#'
#' **Default value** is `val_cName = "value"`.
#'
#' @param keep_NA (optional)
#'
#' Logical argument specifying whether `NA` time series values in the input multivariate time series data frame
#' should be kept in the output stacked data frame.
#'
#' **Default value** is `keep_NA = FALSE`.
#'
#'
#' @returns
#' The function returns a data frame with four variables:
#' * Series name, type character (see argument `ser_cName`)
#' * Data point year, type numeric (see argument `yr_cName`)
#' * Data point period, type numeric (see argument `per_cName`)
#' * Data point value, type numeric (see argument `val_cName`)
#'
#' Note: the function returns a "data.frame" object than can be explicitly coerced to another type of object 
#' with the appropriate `as*()` function (e.g., `tibble::as_tibble()` would coerce it to a tibble).
#'
#'
#' @seealso [unstack_tsDF()] [stack_bmkDF()] [ts_to_tsDF()] [benchmarking()] [stock_benchmarking()]
#'
#'
#' @example misc/function_examples/stack_tsDF-ex.R
#'
#'
#' @export
stack_tsDF <- function(ts_df,
                       ser_cName = "series",
                       yr_cName = "year",
                       per_cName = "period",
                       val_cName = "value",
                       keep_NA = FALSE) {
  
  # Enforce the default R "error" option (`options(error = NULL)`). E.g. this Turns off traceback
  # generated by calls to the stop() function inside internal functions in R Studio.
  ini_error_opt <- getOption("error")
  on.exit(options(error = ini_error_opt))
  options(error = NULL)
  
  # validate object
  if (!is.data.frame(ts_df)) {
    stop("Argument 'ts_df' is not a 'data.frame' object.\n\n", call. = FALSE)
  }
  ts_df <- as.data.frame(ts_df)
  df_cols <- names(ts_df)
  date_cols <- c(yr_cName, per_cName)
  date_args <- c("yr_cName", "per_cName")
  for (ii in seq_along(date_cols)) {
    if (!(date_cols[ii] %in% df_cols)) {
      stop("The input data frame does not contain column \"", date_cols[ii], "\" (argument '",
           date_args[ii], "').\n\n", call. = FALSE)
    }
  }

  ser_list <- setdiff(names(ts_df), date_cols)
  out_df <- data.frame(col1 = character(),
                       col2 = integer(),
                       col3 = integer(),
                       col4 = double(),
                       stringsAsFactors = FALSE)
  for (ser in ser_list) {
    if (gs.validate_arg_logi(keep_NA)) {
      tmp_df <- ts_df[c(date_cols, ser)]
    } else {
      tmp_df <- ts_df[!is.na(ts_df[ser]), c(date_cols, ser)]
    }
    out_df <- rbind(out_df, data.frame(col1 = ser,
                                       col2 = as.integer(tmp_df[[yr_cName]]),
                                       col3 = as.integer(tmp_df[[per_cName]]),
                                       col4 = as.numeric(tmp_df[[ser]]),
                                       stringsAsFactors = FALSE))
  }

  # Set the column names and reset the now names (numbers)
  names(out_df) <- c(ser_cName, date_cols, val_cName)
  row.names(out_df) <- NULL
  out_df
}
