#'@rdname CST_Intbc
#'@title Downscaling using interpolation and bias adjustment.
#'
#'@author J. Ramon, \email{jaume.ramon@bsc.es}
#'
#'@description This function performs a downscaling using an interpolation and a later bias
#'adjustment. It is recommended that the observations are passed already in the target grid.
#'Otherwise, the function will also perform an interpolation of the observed field into the
#'target grid. The coarse scale and observation data can be either global or regional. In the
#'latter case, the region is defined by the user. In principle, the coarse and observation data
#'are intended to be of the same variable, although different variables can also be admitted.
#'
#'@param exp an 's2dv object' containing the experimental field on the
#'coarse scale for which the downscaling is aimed. The object must have, at least,
#'the dimensions latitude, longitude, start date and member. The object is expected to be
#'already subset for the desired region. Data can be in one or two integrated regions, e.g.,
#'crossing the Greenwich meridian. To get the correct results in the latter case,
#'the borders of the region should be specified in the parameter 'region'. See parameter
#''region'.
#'@param obs an 's2dv object' containing the observational field. The object
#'must have, at least, the dimensions latitude, longitude and start date. The object is
#'expected to be already subset for the desired region.
#'@param exp_cor an optional 's2dv_cube' object with named dimensions containing the seasonal 
#'forecast experiment data. If the forecast is provided, it will be downscaled using the 
#'hindcast and observations; if not provided, the hindcast will be downscaled instead. The 
#'default value is NULL.
#'@param target_grid a character vector indicating the target grid to be passed to CDO.
#'It must be a grid recognised by CDO or a NetCDF file.
#'@param bc_method a character vector indicating the bias adjustment method to be applied after
#'the interpolation. Accepted methods are 'quantile_mapping', 'dynamical_bias', 'bias', 'evmos',
#''mse_min', 'crps_min', 'rpc-based'. The abbreviations 'dbc','qm' can also be used.
#'@param int_method a character vector indicating the regridding method to be passed to CDORemap.
#'Accepted methods are "con", "bil", "bic", "nn", "con2". If "nn" method is to be used, CDO_1.9.8
#'or newer version is required.
#'@param points a list of two elements containing the point latitudes and longitudes
#'of the locations to downscale the model data. The list must contain the two elements
#'named as indicated in the parameters 'lat_dim' and 'lon_dim'. If the downscaling is
#'to a point location, only regular grids are allowed for exp and obs. Only needed if the
#'downscaling is to a point location.
#'@param method_point_interp a character vector indicating the interpolation method to interpolate
#'model gridded data into the point locations. Accepted methods are "nearest", "bilinear", "9point",
#'"invdist4nn", "NE", "NW", "SE", "SW". Only needed if the downscaling is to a point location.
#'@param lat_dim a character vector indicating the latitude dimension name in the element 'data'
#'in exp and obs. Default set to "lat".
#'@param lon_dim a character vector indicating the longitude dimension name in the element 'data'
#'in exp and obs. Default set to "lon".
#'@param sdate_dim a character vector indicating the start date dimension name in the element
#''data' in exp and obs. Default set to "sdate".
#'@param member_dim a character vector indicating the member dimension name in the element
#''data' in exp and obs. Default set to "member".
#'@param region a numeric vector indicating the borders of the region defined in obs.
#'It consists of four elements in this order: lonmin, lonmax, latmin, latmax. lonmin refers
#'to the left border, while lonmax refers to the right border. latmin indicates the lower
#'border, whereas latmax indicates the upper border. If set to NULL (default), the function
#'takes the first and last elements of the latitudes and longitudes.
#'@param ncores an integer indicating the number of cores to use in parallel computation. 
#'The default value is NULL.
#'@return An 's2dv' object. The element 'data' contains the dowscaled field, 'lat' the
#'downscaled latitudes, and 'lon' the downscaled longitudes.
#'@examples
#'exp <- rnorm(500)
#'dim(exp) <- c(member = 5, lat = 4, lon = 5, sdate = 5)
#'exp_lons <- 1:5
#'exp_lats <- 1:4
#'obs <- rnorm(900)
#'dim(obs) <- c(lat = 12, lon = 15, sdate = 5)
#'obs_lons <- seq(1,5, 4/14)
#'obs_lats <- seq(1,4, 3/11)
#'exp <- s2dv_cube(data = exp, coords = list(lat = exp_lats, lon = exp_lons))
#'obs <- s2dv_cube(data = obs, coords = list(lat = obs_lats, lon = obs_lons))
#'res <- CST_Intbc(exp = exp, obs = obs, target_grid = 'r1280x640', bc_method = 'bias', int_method = 'conservative')
#'@export

CST_Intbc <- function(exp, obs, exp_cor = NULL, target_grid, bc_method, int_method = NULL, 
                      points = NULL, method_point_interp = NULL, lat_dim = "lat", lon_dim = "lon", 
                      sdate_dim = "sdate", member_dim = "member", time_dim = "time",
                      region = NULL, ncores = NULL, ...)
{
  if (!inherits(exp,'s2dv_cube')) {
    stop("Parameter 'exp' must be of the class 's2dv_cube'")
  }

  if (!inherits(obs,'s2dv_cube')) {
    stop("Parameter 'obs' must be of the class 's2dv_cube'")
  }

  res <- Intbc(exp = exp$data, obs = obs$data, exp_cor = exp_cor$data, 
               exp_lats = exp$coords[[lat_dim]], exp_lons = exp$coords[[lon_dim]], 
               obs_lats = obs$coords[[lat_dim]], obs_lons = obs$coords[[lon_dim]], 
               target_grid = target_grid, int_method = int_method, 
               bc_method = bc_method, points = points, 
               source_file_exp = exp$attrs$source_files[1], 
               source_file_obs = obs$attrs$source_files[1], 
               method_point_interp = method_point_interp, lat_dim = lat_dim, lon_dim = lon_dim, 
               sdate_dim = sdate_dim, member_dim = member_dim, 
               time_dim = time_dim, region = region, ncores = ncores, ...)

  # Modify data, lat and lon in the origina s2dv_cube, adding the downscaled data
  obs$data <- res$obs
  obs$dims <- dim(obs$data)
  obs$coords[[lon_dim]] <- res$lon
  obs$coords[[lat_dim]] <- res$lat

  if (is.null(exp_cor)) {
    exp$data <- res$data
    exp$dims <- dim(exp$data)
    exp$coords[[lon_dim]] <- res$lon
    exp$coords[[lat_dim]] <- res$lat
  
    res_s2dv <- list(exp = exp, obs = obs)
  } else {
    exp_cor$data <- res$data
    exp_cor$dims <- dim(exp_cor$data)
    exp_cor$coords[[lon_dim]] <- res$lon
    exp_cor$coords[[lat_dim]] <- res$lat
  
    res_s2dv <- list(exp = exp_cor, obs = obs)
  }

  return(res_s2dv)
  
}

#'@rdname Intbc
#'@title Downscaling using interpolation and bias adjustment.
#' 
#'@author J. Ramon, \email{jaume.ramon@bsc.es}
#'
#'@description This function performs a downscaling using an interpolation and a later bias 
#'adjustment. It is recommended that the observations are passed already in the target grid. 
#'Otherwise, the function will also perform an interpolation of the observed field into the 
#'target grid. The coarse scale and observation data can be either global or regional. In the 
#'latter case, the region is defined by the user. In principle, the coarse and observation data 
#'are intended to be of the same variable, although different variables can also be admitted. 
#'
#'@param exp an array with named dimensions containing the experimental field on the
#'coarse scale for which the downscaling is aimed. The object must have, at least,
#'the dimensions latitude, longitude, start date and member. The object is expected to be 
#'already subset for the desired region. Data can be in one or two integrated regions, e.g.,
#'crossing the Greenwich meridian. To get the correct results in the latter case,
#'the borders of the region should be specified in the parameter 'region'. See parameter
#''region'.
#'@param obs an array with named dimensions containing the observational field. The object 
#'must have, at least, the dimensions latitude, longitude and start date. The object is 
#'expected to be already subset for the desired region.
#'@param exp_cor an optional array with named dimensions containing the seasonal forecast
#'experiment data. If the forecast is provided, it will be downscaled using the hindcast and 
#'observations; if not provided, the hindcast will be downscaled instead. The default value 
#'is NULL. 
#'@param exp_lats a numeric vector containing the latitude values in 'exp'. Latitudes must 
#'range from -90 to 90.
#'@param exp_lons a numeric vector containing the longitude values in 'exp'. Longitudes 
#'can range from -180 to 180 or from 0 to 360.
#'@param obs_lats a numeric vector containing the latitude values in 'obs'. Latitudes must
#'range from -90 to 90.
#'@param obs_lons a numeric vector containing the longitude values in 'obs'. Longitudes
#'can range from -180 to 180 or from 0 to 360.
#'@param target_grid a character vector indicating the target grid to be passed to CDO.
#'It must be a grid recognised by CDO or a NetCDF file.
#'@param bc_method a character vector indicating the bias adjustment method to be applied after
#'the interpolation. Accepted methods are 'quantile_mapping', 'dynamical_bias', 'bias', 'evmos',
#''mse_min', 'crps_min', 'rpc-based'. The abbreviations 'dbc','qm' can also be used.
#'@param int_method a character vector indicating the regridding method to be passed to CDORemap.
#'Accepted methods are "con", "bil", "bic", "nn", "con2". If "nn" method is to be used, CDO_1.9.8
#'or newer version is required.
#'@param points a list of two elements containing the point latitudes and longitudes
#'of the locations to downscale the model data. The list must contain the two elements
#'named as indicated in the parameters 'lat_dim' and 'lon_dim'. If the downscaling is
#'to a point location, only regular grids are allowed for exp and obs. Only needed if the
#'downscaling is to a point location.
#'@param method_point_interp a character vector indicating the interpolation method to interpolate 
#'model gridded data into the point locations. Accepted methods are "nearest", "bilinear", "9point", 
#'"invdist4nn", "NE", "NW", "SE", "SW". Only needed if the downscaling is to a point location.
#'@param lat_dim a character vector indicating the latitude dimension name in the element 'data' 
#'in exp and obs. Default set to "lat".
#'@param lon_dim a character vector indicating the longitude dimension name in the element 'data' 
#'in exp and obs. Default set to "lon".
#'@param sdate_dim a character vector indicating the start date dimension name in the element 
#''data' in exp and obs. Default set to "sdate".
#'@param member_dim a character vector indicating the member dimension name in the element
#''data' in exp and obs. Default set to "member". 
#'@param source_file_exp a character vector with a path to an example file of the exp data.
#'Only needed if the downscaling is to a point location.
#'@param source_file_obs a character vector with a path to an example file of the obs data.
#'Only needed if the downscaling is to a point location.
#'@param region a numeric vector indicating the borders of the region defined in obs.
#'It consists of four elements in this order: lonmin, lonmax, latmin, latmax. lonmin refers
#'to the left border, while lonmax refers to the right border. latmin indicates the lower
#'border, whereas latmax indicates the upper border. If set to NULL (default), the function
#'takes the first and last elements of the latitudes and longitudes.
#'@param ncores an integer indicating the number of cores to use in parallel computation. 
#'The default value is NULL.
#'@import CSTools 
#'
#'@seealso \code{\link[CSTools]{BiasCorrection}}
#'@seealso \code{\link[CSTools]{Calibration}}
#'@seealso \code{\link[CSTools]{QuantileMapping}}
#'
#'@return An list of three elements. 'data' contains the dowscaled field, 'lat' the
#'downscaled latitudes, and 'lon' the downscaled longitudes.
#'@examples 
#'exp <- rnorm(500) 
#'dim(exp) <- c(member = 5, lat = 4, lon = 5, sdate = 5) 
#'exp_lons <- 1:5 
#'exp_lats <- 1:4 
#'obs <- rnorm(900) 
#'dim(obs) <- c(lat = 12, lon = 15, sdate = 5) 
#'obs_lons <- seq(1,5, 4/14) 
#'obs_lats <- seq(1,4, 3/11) 
#'res <- Intbc(exp = exp, obs = obs, exp_lats = exp_lats, exp_lons = exp_lons, obs_lats = obs_lats, 
#'obs_lons = obs_lons, target_grid = 'r1280x640', bc_method = 'simple_bias', int_method = 'conservative')
#'@export
Intbc <- function(exp, obs, exp_cor = NULL, exp_lats, exp_lons, obs_lats, obs_lons, target_grid, 
                  bc_method, int_method = NULL, points = NULL, method_point_interp = NULL, 
                  lat_dim = "lat", lon_dim = "lon", sdate_dim = "sdate", 
                  time_dim = "time", member_dim = "member", source_file_exp = NULL, 
                  source_file_obs = NULL, region = NULL, ncores = NULL, ...) {

  if (!inherits(bc_method, 'character')) {
    stop("Parameter 'bc_method' must be of the class 'character'")
  }

  if (!inherits(lat_dim, 'character')) {
    stop("Parameter 'lat_dim' must be of the class 'character'")
  }

  if (!inherits(lon_dim, 'character')) {
    stop("Parameter 'lon_dim' must be of the class 'character'")
  }

  if (!inherits(sdate_dim, 'character')) {
    stop("Parameter 'sdate_dim' must be of the class 'character'")
  }

  if (!inherits(member_dim, 'character')) {
    stop("Parameter 'member_dim' must be of the class 'character'")
  }

  if (is.na(match(lon_dim, names(dim(exp))))) {
    stop("Missing longitude dimension in 'exp', or does not match the parameter ",
         "'lon_dim'")
  }

  if (is.na(match(lat_dim, names(dim(exp))))) {
    stop("Missing latitude dimension in 'exp', or does not match the parameter ",
         "'lat_dim'")
  }

  if (is.na(match(sdate_dim, names(dim(exp)))) | is.na(match(sdate_dim, names(dim(obs))))) {
    stop("Missing start date dimension in 'exp' and/or 'obs', or does not match the parameter ",
         "'sdate_dim'")
  }

  if (is.na(match(member_dim, names(dim(exp))))) {
    stop("Missing member dimension in 'exp', or does not match the parameter 'member_dim'")
  }

  if (!(bc_method %in% c('qm', 'dbc', 'quantile_mapping', 'dynamical_bias', 'bias', 'evmos', 'mse_min', 
       'crps_min', 'rpc-based'))) {
    stop("Parameter 'bc_method' must be a character vector indicating the bias adjustment method. ",
         "Accepted methods are 'quantile_mapping', 'dynamical_bias', 'bias', 'evmos', 'mse_min', ",
         "'crps_min', 'rpc-based'. The abbreviations 'dbc','qm' can also be used.")
  }

  if (!is.null(exp_cor)) {
    if (is.na(match(lon_dim, names(dim(exp_cor))))) {
      stop("Missing longitude dimension in 'exp_cor', or does not match the parameter ",
           "'lon_dim'")
    }

    if (is.na(match(lat_dim, names(dim(exp_cor))))) {
      stop("Missing latitude dimension in 'exp_cor', or does not match the parameter ",
           "'lat_dim'")
    }

    if (is.na(match(sdate_dim, names(dim(exp_cor))))) {
      stop("Missing start date dimension in 'exp_cor', or does not match the parameter ",
           "'sdate_dim'")
    }

    if (is.na(match(member_dim, names(dim(exp_cor))))) {
      stop("Missing member dimension in 'exp_cor', or does not match the parameter 'member_dim'")
    }
  }

  # When observations are pointwise
  if (!is.null(points) & !is.na(match("location", names(dim(obs))))) {
    point_obs <- T
    # dimension aux in obs is needed 
    if (is.na(match("aux", names(dim(obs))))) {
      obs <- InsertDim(obs, posdim = 1, lendim = 1, name = "aux")
    }
  } else {
    point_obs <- F
  }
 
  if (!is.null(points) & (is.null(source_file_exp))) {
    stop("No source file found. Source file must be provided in the parameter 'source_file_exp'.")
  }

  if (!is.null(points) & is.null(method_point_interp)) {
    stop("Please provide the interpolation method to interpolate gridded data to point locations ",
         "through the parameter 'method_point_interp'.")
  }

  if (is.null(region)) {
    warning("The borders of the downscaling region have not been provided. Assuming the ", 
            "four borders of the downscaling region are defined by the first and last ",
            "elements of the parametres 'obs_lats' and 'obs_lons'.")
    region <- c(obs_lons[1], obs_lons[length(obs_lons)], obs_lats[1], obs_lats[length(obs_lats)])
  }
  ## ncores
  if (!is.null(ncores)) {
    if (!is.numeric(ncores) | any(ncores %% 1 != 0) | any(ncores < 0) |
        length(ncores) > 1) {
      stop("Parameter 'ncores' must be a positive integer.")
    }
  }
  
  exp_interpolated <- Interpolation(exp = exp, lats = exp_lats, lons = exp_lons, 
                                    target_grid = target_grid, method_remap = int_method, 
                                    points = points, source_file = source_file_exp,
                                    lat_dim = lat_dim, lon_dim = lon_dim, 
                                    method_point_interp = method_point_interp, 
                                    region = region, ncores = ncores)

  exp_cor_interpolated <- NULL

  if (!is.null(exp_cor)) {
    exp_cor_interpolated <- Interpolation(exp = exp_cor, lats = exp_lats, lons = exp_lons, 
                                          target_grid = target_grid, method_remap = int_method, 
                                          points = points, source_file = source_file_exp,
                                          lat_dim = lat_dim, lon_dim = lon_dim, 
                                          method_point_interp = method_point_interp,
                                          region = region, ncores = ncores)

  } 
 
  # If after interpolating 'exp' data the coordinates do not match, the obs data is interpolated to
  # the same grid to force the matching
  if ((!.check_coords(lat1 = exp_interpolated$lat, lat2 = obs_lats,
                      lon1 = exp_interpolated$lon, lon2 = obs_lons)) | !(point_obs)) {
    obs_interpolated <- Interpolation(exp = obs, lats = obs_lats, lons = obs_lons, 
                                      target_grid = target_grid, method_remap = int_method, 
                                      points = points, source_file = source_file_obs,
                                      lat_dim = lat_dim, lon_dim = lon_dim, 
                                      method_point_interp = method_point_interp, 
                                      region = region, ncores = ncores)
    obs_ref <- obs_interpolated$data
  } else {
    obs_ref <- obs
  }

  # Some functions only accept the dimension names "member" and "sdate" for exp and 
  # "sdate" for obs
  #if (member_dim != 'member') {
  #  names(dim(exp_interpolated$data)) <- replace(names(dim(exp_interpolated$data)),
  #    which(names(dim(exp_interpolated$data)) == member_dim), 'member')
  #}

  #if (sdate_dim != 'sdate') {
  #  names(dim(exp_interpolated$data)) <- replace(names(dim(exp_interpolated$data)),
  #    which(names(dim(exp_interpolated$data)) == sdate_dim), 'sdate')
  #  names(dim(obs_ref)) <- replace(names(dim(obs_ref)),
  #    which(names(dim(obs_ref)) == sdate_dim), 'sdate')
  #}


  k_out <- loop <- 1 # the data is NOT hourly/daily/weekly, i.e., time = 1. Apply leave one data out.
  daily <- FALSE # for the case at which time > 1 or for the case of forecast dwn (exp_cor) 
  eval.method <- "leave-one-out"
  # if time > 1, apply leave-one-year-out
  if (  time_dim %in% names(dim(obs_ref)) & dim(obs_ref)[time_dim] > 1 & is.null(exp_cor)) {
    daily <- TRUE
    eval.method <- "in-sample"
    sdate_num <- as.numeric (dim(obs_ref)[sdate_dim])
    k_out <- as.numeric (dim(obs_ref)[time_dim]) ## in case loocv = TRUE and the data is daily, leave one-year data out
    obs_ref <-  MergeDims(obs_ref, merge_dims = c(time_dim, sdate_dim), rename_dim = sdate_dim)
    exp_interpolated$data <- MergeDims(exp_interpolated$data, 
                                       merge_dims = c(time_dim, sdate_dim), rename_dim = sdate_dim)
    loop <- dim(obs_ref)[sdate_dim]/k_out
  } else if (!is.null(exp_cor)) {
    eval.method <- "in-sample"
  }

  sdate_num <- as.numeric (dim(obs_ref)[sdate_dim])
  res <- list()

  for (yy in 1:loop) {  # with cv purposes. leave-one-year-out

    if (loop == 1) {
      window <- -(1:sdate_num)
      EXP_COR <- NULL
      if (!is.null(exp_cor)) {
        EXP_COR <- exp_cor_interpolated$data
      }
    } else {
      window <- ((yy-1) * k_out+1):((yy-1) * k_out + k_out)
      EXP_COR <- ClimProjDiags::Subset(exp_interpolated$data, along = sdate_dim,
                                       indices = (1:sdate_num)[window], drop = "selected")
    }

    OBS_REF <- ClimProjDiags::Subset(obs_ref, along = sdate_dim, 
                                     indices = (1:sdate_num)[-window], drop = "selected")
    EXP <- ClimProjDiags::Subset(exp_interpolated$data, along = sdate_dim, 
                                 indices = (1:sdate_num)[-window], drop = "selected")

    if (bc_method == 'qm' | bc_method == 'quantile_mapping') {
      dum <- QuantileMapping(exp = EXP, obs = OBS_REF, 
                             exp_cor = EXP_COR,
                             na.rm = TRUE,  memb_dim = member_dim, sdate_dim = sdate_dim, 
                             ncores = ncores, eval.method = eval.method, ...)

      res[[yy]] <- dum
      rm(dum)
    }
    else if (bc_method == 'dbc' | bc_method == 'dynamical_bias') {
      # Dynamical bias correction is not yet prepared to handle hindcast-forecast data
      # It will return only the hindcast downscaled
      # Dynamical bias correction is not yet prepared to handle leave-one-year-out for daily case
      # It will properly work only with the monthly or coarser temporal scale data. 
      if (!is.null(exp_cor)) {
        warning("For the dynamical bias correction only the hindcast downscaled will be returned.")
      }
      # the temporal dimension must be only one dimension called "time"
      if (all(c(time_dim, sdate_dim) %in% names(dim(exp_interpolated$data)))) {
        exp_interpolated$data <- Apply(exp_interpolated$data, target_dims = c(time_dim, sdate_dim), 
        fun = as.vector, output_dims = "time", ncores = ncores)$output1
      } 
      if (all(c(time_dim, sdate_dim) %in% names(dim(obs_ref)))) {
        obs_ref <- Apply(obs_ref, target_dims = c(time_dim, sdate_dim), fun = as.vector, 
          output_dims = "time", ncores = ncores)$output1
      }
      # REMEMBER to add na.rm = T in colMeans in .proxiesattractor 
      dum <- DynBiasCorrection(exp = EXP, obs = OBS_REF, ncores = ncores, ...)
      res[[yy]] <- dum
      rm(dum)
    } else {
      if (dim(exp_interpolated$data)[member_dim] == 1) {
        stop('Calibration must not be used with only one ensemble member.')
      }
      if (dim(obs_ref)[sdate_dim] == 1) {
        warning('Simple Bias Correction should not be used with only one observation. Returning NA.')
      }
      dum <- Calibration(exp = EXP, obs = OBS_REF, 
                         exp_cor = EXP_COR, 
                         memb_dim = member_dim, sdate_dim = sdate_dim, ncores = ncores, 
                         cal.method = bc_method,
	  	         eval.method = eval.method) 
      res[[yy]] <- dum
      rm(dum)
    }
  } 

  require (abind)  
  dimnames <- names(dim(res[[1]]))
  res <- do.call(abind, c(res, along = which(names(dim(res[[1]])) == "syear")))
  names(dim(res)) <- dimnames

  if (daily) {
    res <- SplitDim(res, split_dim = sdate_dim, new_dim_name = time_dim,
                    indices = rep(1:k_out, sdate_num/k_out))
    obs_ref <- SplitDim(obs_ref, split_dim = sdate_dim, new_dim_name = time_dim,
                        indices = rep(1:k_out, sdate_num/k_out))
  }
    
  # Return a list of three elements
  res <- list(data = res, obs = obs_ref, lon = exp_interpolated$lon, lat = exp_interpolated$lat)
  return(res)
}
