#'Random Walk test for skill differences
#'
#'Forecast comparison of the skill obtained with 2 forecasts (with respect to a 
#'common observational reference) based on Random Walks (DelSole and Tippett, 2016).
#'
#'@param skill_A A numerical array of the time series of the scores obtained
#'  with the forecaster A.
#'@param skill_B A numerical array of the time series of the scores obtained
#'  with the forecaster B. The dimensions should be identical as parameter 'skill_A'.
#'@param time_dim A character string indicating the name of the dimension along
#'  which the tests are computed. The default value is 'sdate'.
#'@param N.eff Effective sample size to be used in the statistical significance 
#'  test. It can be NA (to use the length of the "time_dim" dimension) or an
#'  array with the same dimensions as "skill_A" except "time_dim" (for a 
#'  particular N.eff to be used for each case). The default value is NA.
#'@param test.type A character string indicating the type of significance test. 
#'  It can be "two.sided.approx" (to assess whether forecaster A and forecaster B are 
#'  significantly different in terms of skill with a two-sided test using the 
#'  approximation of DelSole and Tippett, 2016), "two.sided" (to assess whether 
#'  forecaster A and forecaster B are significantly different in terms of skill 
#'  with a exact two-sided test), "greater" (to assess whether forecaster A shows
#'  significantly better skill than forecaster B with a one-sided test for 
#'  negatively oriented scores), or "less" (to assess whether forecaster A shows
#'  significantly better skill than forecaster B with a one-sided test for 
#'  positively oriented scores). The default value is "two.sided.approx".
#'@param alpha A numeric of the significance level to be used in the statistical
#'  significance test. If it is a numeric, the significance will be returned. 
#'  If NULL, the p-value will be returned instead. The default value is 0.05.
#'@param ncores An integer indicating the number of cores to use for parallel 
#'  computation. The default value is NULL.
#'
#'@return A list with:
#'\item{$score}{
#'  A numerical array with the same dimensions as the input arrays except 
#'  'time_dim'. The number of times that forecaster A has been better than 
#'  forecaster B minus the number of times that forecaster B has been better 
#'  than forecaster A (for skill negatively oriented, i.e., the lower the 
#'  better). If $score is positive, forecaster A has been better more times
#'  than forecaster B. If $score is negative, forecaster B has been better more
#'  times than forecaster A.
#'}
#'\item{$signif}{
#'  A logical array of the statistical significance with the same dimensions
#'  as the input arrays except "time_dim". Returned only if "alpha" is a numeric.
#'}
#'\item{$p.val}{
#'  A numeric array of the p-values with the same dimensions as the input arrays
#'  except "time_dim". Returned only if "alpha" is NULL.
#'}
#'
#'@details 
#' Null and alternative hypothesis for "two-sided" test (regardless of the 
#' orientation of the scores):
#' H0: forecaster A and forecaster B are not different in terms of skill
#' H1: forecaster A and forecaster B are different in terms of skill
#' 
#' Null and alternative hypothesis for one-sided "greater" (for negatively 
#' oriented scores, i.e., the lower the better) and "less" (for positively 
#' oriented scores, i.e., the higher the better) tests:
#' H0: forecaster A is not better than forecaster B
#' H1: forecaster A is better than forecaster B
#' 
#' Examples of negatively oriented scores are the RPS, RMSE and the Error, while
#' the ROC score is a positively oriented score.
#' 
#' DelSole and Tippett (2016) approximation for two-sided test at 95% confidence 
#' level: significant if the difference between the number of times that 
#' forecaster A has been better than forecaster B and forecaster B has been
#' better than forecaster A is above 2sqrt(N) or below -2sqrt(N).
#'
#'@references 
#'DelSole and Tippett (2016): https://doi.org/10.1175/MWR-D-15-0218.1
#'
#'@examples
#' fcst_A <- array(data = 11:50, dim = c(sdate = 10, lat = 2, lon = 2))
#' fcst_B <- array(data = 21:60, dim = c(sdate = 10, lat = 2, lon = 2))
#' reference <- array(data = 1:40, dim = c(sdate = 10, lat = 2, lon = 2))
#' scores_A <- abs(fcst_A - reference)
#' scores_B <- abs(fcst_B - reference)
#' RandomWalkTest(skill_A = scores_A, skill_B = scores_B)
#'
#'@import multiApply
#'@importFrom ClimProjDiags Subset
#'@export
RandomWalkTest <- function(skill_A, skill_B, time_dim = 'sdate', N.eff = NA, 
                           test.type = 'two.sided.approx', alpha = 0.05, ncores = NULL){
  
  ## Check inputs
  ## skill_A and skill_B
  if (is.null(skill_A) | is.null(skill_B)){
    stop("Parameters 'skill_A' and 'skill_B' cannot be NULL.")
  }
  if(!is.numeric(skill_A) | !is.numeric(skill_B)){
    stop("Parameters 'skill_A' and 'skill_B' must be a numerical array.")
  }
  if (!identical(dim(skill_A),dim(skill_B))){
    stop("Parameters 'skill_A' and 'skill_B' must have the same dimensions.")
  }
  ## time_dim
  if(!is.character(time_dim) | length(time_dim) != 1){
    stop("Parameter 'time_dim' must be a character string.")
  }
  if(!time_dim %in% names(dim(skill_A)) | !time_dim %in% names(dim(skill_B))){
    stop("Parameter 'time_dim' is not found in 'skill_A' or 'skill_B' dimensions.")
  }
  ## N.eff
  if (is.array(N.eff)) {
    if (!is.numeric(N.eff)) stop("Parameter 'N.eff' must be numeric.")
    if (!all(names(dim(N.eff)) %in% names(dim(skill_A))) |
        any(dim(skill_A)[match(names(dim(N.eff)), names(dim(skill_A)))] != dim(N.eff))) {
      stop(paste0('If parameter "N.eff" is provided with an array, it must ',
                  'have the same dimensions as "skill_A" except "time_dim".'))
    }
  } else if (!is.na(N.eff)) {
    stop(paste0('Parameter "N.eff" must be NA or an array with ',
                'the same dimensions as "skill_A" except "time_dim".'))
  }
  ## test.type
  if (!test.type %in% c('two.sided.approx','two.sided','greater','less')) {
    stop(paste0("Parameter 'test.type' must be 'two.sided.test' (for two-sided test ",
                "using the aproximation of DelSole and Tippett (2016), regardless ",
                "of the orientation of the scores), 'two.sided' (for two-sided test ",
                "regardless of the orientation of the scores), 'greater' (for ",
                "one-sided test for negatively oriented scores, i.e., the lower ",
                "the better) or 'less' (for one-sided test for positively ",
                "oriented scores, i.e., the higher the better)."))
  }
  if (test.type == 'two.sided.approx'){
    if (is.numeric(alpha) & alpha != 0.05){
      warning(paste0("DelSole and Tippett (2016) aproximation is valid for alpha ",
                     "= 0.05. Returning the significance at the 0.05 significance level."))
    } else if (is.null(alpha)){
      warning(paste0("p-value cannot be returned with the DelSole and Tippett (2016) ",
                     "aproximation. Returning the significance at the 0.05 significance level."))    }
  }
  ## alpha
  if (!is.null(alpha)) {
    if (any(!is.numeric(alpha) | alpha <= 0 | alpha >= 1 |  
            length(alpha) > 1)) {
      stop('Parameter "alpha" must be NULL or a number between 0 and 1.')
    }
  }
  ## ncores
  if (!is.null(ncores)){
    if (!is.numeric(ncores) | ncores %% 1 != 0 | ncores <= 0 | length(ncores) > 1){
      stop("Parameter 'ncores' must be a positive integer.")
    }
  }
  
  ## Compute the Random Walk Test
  res <- multiApply::Apply(data = list(skill_A = skill_A,
                                       skill_B = skill_B,
                                       N.eff = N.eff), 
                           target_dims = list(skill_A = time_dim,
                                              skill_B = time_dim, 
                                              N.eff = NULL),
                           fun = .RandomWalkTest, 
                           test.type = test.type,
                           alpha = alpha,
                           ncores = ncores)
  
  ## to remove the extra dimension created because of target_dims(N.eff) = NULL
  for (n in names(res)){ ## there may be a better way of doing this
    if (!identical(which(names(dim(res[[n]])) == ''),integer(0))){
      res[[n]] <- ClimProjDiags::Subset(x = res[[n]], along = which(names(dim(res[[n]])) == ''), indices = 1, drop = 'selected')
    }
  }
  
  return(res)
}

.RandomWalkTest <- function(skill_A, skill_B, N.eff = NA, test.type = 'two.sided.approx', alpha = 0.05){
  
  if (is.na(N.eff)){
    N.eff <- length(skill_A)
  }
  
  A_better <- sum(skill_B > skill_A)
  B_better <- sum(skill_B < skill_A)
  
  output <- NULL
  output$score <- A_better - B_better
  
  if (test.type == 'two.sided.approx'){
    
    output$signif <- ifelse(test = abs(output$score) > 2*sqrt(N.eff), yes = TRUE, no = FALSE)
    
  } else {
    
    if (!is.na(output$score)){
      if (is.null(alpha)){alpha <- 0.05} ## it is needed for binom.test(), but it does not affect the results since we only retrieve $p.value
      p.val <- binom.test(x = A_better, n = floor(N.eff), p = 0.5, conf.level = 1-alpha, alternative = test.type)$p.value
    } else {p.val <- NA}
    
    if (is.null(alpha)){
      output$p.val <- p.val
    } else {
      output$signif <- ifelse(!is.na(p.val) & p.val <= alpha, TRUE, FALSE)
    }
    
  }
  
  return(output)
}
