#' Convert reconciliation metadata
#' 
#' 
#' @description
#'
#' \if{html,text}{(\emph{version française: 
#' \url{https://StatCan.github.io/gensol-gseries/fr/reference/rkMeta_to_blSpecs.html}})}
#' 
#' Convert a [tsraking()] metadata data frame to a [tsbalancing()] problem specs data frame.
#'
#'
#' @usage
#' rkMeta_to_blSpecs(
#'   metadata_df,
#'   alterability_df = NULL,
#'   alterSeries = 1, 
#'   alterTotal1 = 0,
#'   alterTotal2 = 0,
#'   alterability_df_only = FALSE
#' )
#'
#'
#' @inheritParams tsraking
#' 
#' @param alterability_df_only (optional)
#'
#' Logical argument specifying whether or not only the set of alterability ceofficients found in the alterability 
#' file (argument `alterability_df`) should be included in the returned [tsbalancing()] problem specs data frame.
#' When `alterability_df_only = FALSE` (the default), the alterability coefficients specified with arguments 
#' `alterSeries`, `alterTotal1` and `alterTotal2` are combined with those found in `alterability_df` (the latter 
#' coefficients overwriting the former) and the returned data frame therefore contains alterability coefficients 
#' for all component and cross-sectional control total series. This argument does not affect the set of temporal 
#' total alterability coefficients (associated to [tsraking()] argument `alterAnnual`) that are included in the 
#' returned [tsbalancing()] problem specs data frame. The latter always strictly contains those specified in 
#' `metadata_df` with a non-missing (non-`NA`) value for column `alterAnnual`.
#'
#' **Default value** is `alterability_df_only = FALSE`.
#'
#'
#' @details
#' 
#' The preceding description of argument `alterability_df` comes from [tsraking()]. This function 
#' (`rkMeta_to_blSpecs()`) slightly changes the specification of alterability coefficients with argument 
#' `alterability_df` by allowing either
#' * a single observation, specifying the set of alterability coefficients to use for all periods, 
#' * or one or several observations with an additional column named `timeVal` allowing the specification of 
#' both period-specific alterability coefficients (`timeVal` is not `NA`) and generic coefficients to use for 
#' all other periods (`timeVal` is `NA`). Values for column `timeVal` correspond to the *time values* 
#' of a "ts" object as returned by [stats::time()], conceptually corresponding to \eqn{year + (period - 1) / 
#' frequency}.
#' 
#' Another difference with [tsraking()] is that missing (`NA`) values are allowed in the alterability coefficients 
#' data frame (argument `alterability_df`) and result in using the generic coefficients (observations for which 
#' `timeVal` is `NA`) or the default coefficients (arguments `alterSeries`, `alterTotal1` and `alterTotal2`).
#'
#' Note that apart from discarding alterability coefficients for series not listed in the [tsraking()] metadata 
#' data frame (argument `metadata_df`), this function does not validate the values specified in the alterability 
#' coefficients data frame (argument `alterability_df`) nor the ones specified with column `alterAnnual` in the 
#' [tsraking()] metadata data frame (argument `metadata_df`). The function transfers them *as is* in the 
#' returned [tsbalancing()] problem specs data frame.
#'
#'
#' @returns
#' A [tsbalancing()] problem specs data frame (argument `problem_specs_df`).
#' 
#' 
#' @seealso [tsraking()] [tsbalancing()]
#'
#'
#' @example misc/function_examples/rkMeta_to_blSpecs-ex.R
#'
#'
#' @export
rkMeta_to_blSpecs <- function(metadata_df,
                              alterability_df = NULL,
                              alterSeries = 1, 
                              alterTotal1 = 0,
                              alterTotal2 = 0,
                              alterability_df_only = 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 `metadata_df`
  if (nchar(deparse1(substitute(metadata_df))) == 0) {
    stop("Argument 'metadata_df' is mandatory (it must be specified).\n\n", call. = FALSE)
  }
  metadata_df <- metadata_df
  if (!is.data.frame(metadata_df)) {
    stop("Argument 'metadata_df' is not a 'data.frame' object.\n\n", call. = FALSE)
  }
  metadata_df <- as.data.frame(metadata_df)
  metaDF_cols <- toupper(names(metadata_df))
  names(metadata_df) <- metaDF_cols
  row.names(metadata_df) <- NULL

  # Validate the alterability coefficients arguments
  tmp <- (unlist(alterSeries))[1]
  if (!identical(alterSeries, tmp) || is.null(tmp) || !is.finite(tmp) || tmp < 0) {
    stop("Argument 'alterSeries' must be a nonnegative real number.\n\n", call. = FALSE)
  }
  tmp <- (unlist(alterTotal1))[1]
  if (!identical(alterTotal1, tmp) || is.null(tmp) || !is.finite(tmp) || tmp < 0) {
    stop("Argument 'alterTotal1' must be a nonnegative real number.\n\n", call. = FALSE)
  }
  tmp <- (unlist(alterTotal2))[1]
  if (!identical(alterTotal2, tmp) || is.null(tmp) || !is.finite(tmp) || tmp < 0) {
    stop("Argument 'alterTotal2' must be a nonnegative real number.\n\n", call. = FALSE)
  }
  
  # Validate `alterability_df_only`
  alterability_df_only <- gs.validate_arg_logi(alterability_df_only)

  
  # Generate a fake series data (data frame)
  data_cols <- unique(gs.cleanup_col_list(metadata_df[intersect(c("SERIES", "TOTAL1", "TOTAL2"), 
                                                             metaDF_cols)]))
  n_ser <- length(data_cols)
  fake_df <- as.data.frame(matrix(rep(0, n_ser), 
                                  nrow = 1,
                                  dimnames = list(NULL, data_cols)))

  
  # Process the alterability coefficients data frame
  if (!is.null(alterability_df)) {
    alterability_df <- alterability_df
    if (is.data.frame(alterability_df)) {
      alterability_df <- as.data.frame(alterability_df)
      alterDF_cols <- toupper(names(alterability_df))
      alter_ser <- setdiff(alterDF_cols, "TIMEVAL")
      names(alterability_df) <- alterDF_cols
      row.names(alterability_df) <- NULL
      
      # Split period-specific (dated) and generic (non-dated) alterability coefficients
      if ("TIMEVAL" %in% alterDF_cols) {
        tmp <- !is.na(alterability_df$TIMEVAL)
        dated_id <- which(tmp)
        if (length(dated_id) > 0) {
          dated_alter_df <- alterability_df[dated_id, , drop = FALSE]
        } else {
          dated_alter_df <- alterability_df[NULL, , drop = FALSE]
        }
        nondated_id <- which(!tmp)
        if (length(nondated_id) > 0) {
          nondated_alter_df <- alterability_df[nondated_id[1], alter_ser, drop = FALSE]
        } else {
          nondated_alter_df <- alterability_df[NULL, alter_ser, drop = FALSE]
        }
        
      } else {
        if (nrow(alterability_df) > 1) {
          warning("The specified 'alterability_df' contains more than one observation but does not include column ", 
                  "`timeVal`. Only the set of alterability coefficients from the first observation will be used.\n", 
                  call. = FALSE, immediate. = TRUE)
          nondated_alter_df <- alterability_df[1, , drop = FALSE]
        } else {
          nondated_alter_df <- alterability_df
        }
        dated_alter_df <- nondated_alter_df
        dated_alter_df$TIMEVAL <- NA_real_
        dated_alter_df <- dated_alter_df[NULL, , drop = FALSE]
      }
      
      # Enforce matching series names (case-wise) and reject series that don't exist in `metadata_df`
      data_cols_uc <- toupper(data_cols)
      alter_col_id <- match(alter_ser, data_cols_uc, nomatch = 0)
      nondated_alter_df <- nondated_alter_df[data_cols_uc[alter_col_id]]
      alter_cols <- data_cols[alter_col_id]
      names(nondated_alter_df) <- alter_cols
      dated_alter_df <- dated_alter_df[c("TIMEVAL", data_cols_uc[alter_col_id])]
      names(dated_alter_df) <- c("TIMEVAL", alter_cols)
      
      # Reject missing (NA) generic (non-dated) alter coefs, if any
      nondated_alter_df <- nondated_alter_df[, !is.na(nondated_alter_df), drop = FALSE]
      if (nrow(nondated_alter_df) == 0 || ncol(nondated_alter_df)  == 0) {
        nondated_alter_df <- NULL
      }
      
    } else {
      # Accept `alterability_df = NA` as `alterability_df = NULL`
      tmp <- (unlist(alterability_df))[1]
      if (!identical(alterability_df, tmp) || !is.na(tmp)) {
        warning("Argument 'alterability_df' is not a 'data.frame' object. It will be ignored.\n",
                call. = FALSE, immediate. = TRUE)
        alterability_df <- NULL
      }
      nondated_alter_df <- NULL
      dated_alter_df <- as.data.frame(NULL)
    }
  } else {
    nondated_alter_df <- NULL
    dated_alter_df <- as.data.frame(NULL)
  }

  
  # Build the elements of the raking problem (while validating the specified info), 
  # excluding the component series temporal totals info:
  #   - x        : vector of component series initial values
  #   - c_x      : vector of component series alterability coefficients
  #   - comp_cols: vector of component series (column) names 
  #   - g        : vector of marginal total series initial values
  #   - c_g      : vector of marginal total series alterability coefficients
  #   - tot_cols : vector of marginal total series (column) names 
  #   - G        : marginal total series aggregation matrix (`g = G %*% x` for coherent/reconciled data)
  #
  # The returned raking problem elements do not include the implicit component series temporal totals 
  # when applicable (i.e., elements `g` and `G` only contain the marginal totals info).
  #
  rk <- build_raking_problem(data_df         = fake_df,
                             metadata_df     = metadata_df,
                             alterability_df = nondated_alter_df,
                             alterSeries     = alterSeries, 
                             alterTotal1     = alterTotal1,
                             alterTotal2     = alterTotal2)
  all_cols <- c(rk$comp_cols, rk$tot_cols)
  
  
  # Start with the balancing constraints 
  n_con <- length(rk$tot_cols)
  specs_df <- data.frame(sort.1 = 1:n_con,
                         sort.2 = rep.int(0, n_con),
                         type = rep.int("EQ", n_con),
                         col = rep.int(NA_character_, n_con),
                         row = paste0("Marginal Total ", 1:n_con, " (", rk$tot_cols, ")"),
                         coef = rep.int(NA_real_, n_con),
                         timeVal = rep.int(NA_real_, n_con))
  
  for (ii in 1:n_con) {
    comp_id <- which(rk$G[ii, ] == 1)
    n_comp <- length(comp_id)
    specs_df <- rbind(specs_df,
                      data.frame(sort.1 = rep.int(ii, n_comp + 1),
                                 sort.2 = 1:(n_comp + 1),
                                 type = rep.int(NA_character_, n_comp + 1),
                                 col = c(rk$comp_cols[comp_id], rk$tot_cols[ii]),
                                 row = rep.int(paste0("Marginal Total ", ii, " (", rk$tot_cols[ii], ")"), n_comp + 1),
                                 coef = c(rep.int(1, n_comp), -1),
                                 timeVal = rep.int(NA_real_, n_comp + 1)))
  }
  
  specs_df <- specs_df[order(specs_df$sort.1, specs_df$sort.2), ]

  
  # Add the generic (non-dated) alterability coefficients (from `alterability_df' only or not)
  if (!alterability_df_only) {
    nondated_alter <- TRUE
    alter_df <- data.frame(sort = 0:n_ser,
                           type = c("alter", rep(NA_character_, n_ser)),
                           col = c(NA_character_, all_cols),
                           row = rep.int("Period Value Alterability", n_ser + 1),
                           coef = c(NA_real_, rk$c_x, rk$c_g),
                           timeVal = rep.int(NA_real_, n_ser + 1))
    
    
  } else if (!is.null(nondated_alter_df)) {  
    nondated_alter <- TRUE
    col_id <- match(names(nondated_alter_df), all_cols)
    n_alter <- length(col_id)
    alter_df <- data.frame(sort = c(0, col_id),
                           type = c("alter", rep(NA_character_, n_alter)),
                           col = c(NA_character_, all_cols[col_id]),
                           row = rep.int("Period Value Alterability", n_alter + 1),
                           coef = c(NA_real_, c(rk$c_x, rk$c_g)[col_id]),
                           timeVal = rep.int(NA_real_, n_alter + 1))

  } else {
    nondated_alter <- FALSE
    alter_df <- NULL
  }
  
  
  # Add the period-specific (dated) alterability coefficients
  if (nrow(dated_alter_df) > 0) {
    
    dated_alter <- FALSE
    for (ii in seq_along(alter_cols)) {
      logi_vec <- !is.na(dated_alter_df[[alter_cols[ii]]])
      n_alter <- sum(logi_vec)
      if (n_alter > 0) {
        dated_alter <- TRUE
        alter_df <- rbind(alter_df,
                          data.frame(sort = rep.int(alter_col_id[ii], n_alter),
                                     type = rep.int(NA_character_, n_alter),
                                     col = rep.int(alter_cols[ii], n_alter),
                                     row = rep.int("Period Value Alterability", n_alter),
                                     coef = dated_alter_df[[alter_cols[ii]]][logi_vec],
                                     timeVal = dated_alter_df[["TIMEVAL"]][logi_vec]))
      }
    }
    
    # Add the label definition record (if needed)
    if (!nondated_alter && dated_alter) {
      alter_df <- rbind(data.frame(sort = 0,
                                   type = "alter",
                                   col = NA_character_,
                                   row = "Period Value Alterability",
                                   coef = NA_real_,
                                   timeVal = NA_real_),
                        alter_df)
    }    
  }
  
    
  # Add the period value alterability coefficients
  if (!is.null(alter_df)) {
    alter_df <- alter_df[order(alter_df$sort, alter_df$timeVal, na.last = FALSE), ]
    specs_df <- rbind(specs_df[, -c(1, 2)], alter_df[, -1])
  } else  {
    specs_df <- specs_df[, -c(1, 2)]
  }

  
  # Add the temporal total alterability coefficients
  if ("ALTERANNUAL" %in% metaDF_cols) {
    logi_vec <- !is.na(metadata_df$ALTERANNUAL)
    n_comp <- sum(logi_vec)
    if (n_comp > 0) {
      specs_df <- rbind(specs_df,
                        data.frame(type = c("alterTmp", rep(NA_character_, n_comp)),
                                   col = c(NA_character_, metadata_df$SERIES[logi_vec]),
                                   row = rep.int("Temporal Total Alterability", n_comp + 1),
                                   coef = c(NA_real_, metadata_df$ALTERANNUAL[logi_vec]),
                                   timeVal = rep(NA_real_, n_comp + 1)))
    }
  }
  
  
  # Reset the row labels
  row.names(specs_df) <- NULL
  specs_df
}
