#' Stack benchmarks data
#'
#'
#' @description
#'
#' \if{html,text}{(\emph{version française: 
#' \url{https://StatCan.github.io/gensol-gseries/fr/reference/stack_bmkDF.html}})}
#' 
#' Convert a multivariate benchmarks data frame (see [ts_to_bmkDF()]) for the benchmarking functions
#' ([benchmarking()] and [stock_benchmarking()]) into a stacked (tall) data frame with six variables (columns):
#' * one (1) for the benchmark name (e.g., series name)
#' * four (4) for the benchmark coverage
#' * one (1) for the benchmark value
#'
#' Missing (`NA`) benchmark 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 bmk_df (mandatory)
#'
#' Data frame (object of class "data.frame") that contains the multivariate benchmarks 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 benchmark names (name of the benchmark variables in the input multivariate benchmarks 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 startYr_cName,startPer_cName,endYr_cName,endPer_cName (optional)
#'
#' Strings specifying the name of the numeric variables (columns) in the input multivariate benchmarks data frame
#' that define the benchmark coverage, i.e., the starting and ending year and period (cycle) identifiers. These variables
#' are *transferred* to the output stacked data frame with the same variable names.
#'
#' **Default values** are `startYr_cName = "startYear"`, `startPer_cName = "startPeriod"`
#' `endYr_cName = "endYear"` and `endPer_cName   = "endPeriod"`.
#'
#' @param val_cName (optional)
#'
#' String specifying the name of the numeric variable (column) in the output stacked data frame that will
#' contain the benchmark values.
#'
#' **Default value** is `val_cName = "value"`.
#'
#' @param keep_NA (optional)
#'
#' Logical argument specifying whether missing (`NA`) benchmark values in the input multivariate benchmarks 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 six variables:
#' * Benchmark (series) name, type character (see argument `ser_cName`)
#' * Benchmark coverage starting year, type numeric (see argument `startYr_cName`)
#' * Benchmark coverage starting period, type numeric (see argument `startPer_cName`)
#' * Benchmark coverage ending year, type numeric (see argument `endtYr_cName`)
#' * Benchmark coverage ending period, type numeric (see argument `endPer_cName`)
#' * Benchmark 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 [stack_tsDF()] [ts_to_bmkDF()] [benchmarking()] [stock_benchmarking()]
#'
#'
#' @example misc/function_examples/stack_bmkDF-ex.R
#'
#'
#' @export
stack_bmkDF <- function(bmk_df,
                        ser_cName = "series",
                        startYr_cName = "startYear",
                        startPer_cName = "startPeriod",
                        endYr_cName = "endYear",
                        endPer_cName = "endPeriod",
                        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(bmk_df)) {
    stop("Argument 'bmk_df' is not a 'data.frame' object.\n\n", call. = FALSE)
  }
  bmk_df <- as.data.frame(bmk_df)
  df_cols <- names(bmk_df)
  date_cols <- c(startYr_cName, startPer_cName, endYr_cName, endPer_cName)
  date_args <- c("startYr_cName", "startPer_cName", "endYr_cName", "endPer_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(df_cols, date_cols)
  out_df <- data.frame(col1 = character(),
                       col2 = integer(),
                       col3 = integer(),
                       col4 = integer(),
                       col5 = integer(),
                       col6 = double(),
                       stringsAsFactors = FALSE)
  for (ser in ser_list) {
    if (gs.validate_arg_logi(keep_NA)) {
      tmp_df <- bmk_df[c(date_cols, ser)]
    } else {
      tmp_df <- bmk_df[!is.na(bmk_df[ser]), c(date_cols, ser)]
    }
    out_df <- rbind(out_df, data.frame(col1 = ser,
                                       col2 = as.integer(tmp_df[[startYr_cName]]),
                                       col3 = as.integer(tmp_df[[startPer_cName]]),
                                       col4 = as.integer(tmp_df[[endYr_cName]]),
                                       col5 = as.integer(tmp_df[[endPer_cName]]),
                                       col6 = 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  
}
