#'@rdname CST_Intlr
#'@title Downscaling using interpolation and linear regression.
#' 
#'@author J. Ramon, \email{jaume.ramon@bsc.es}
#'
#'@description This function performs a downscaling using an interpolation and a linear 
#'regression. Different methodologies that employ linear regressions are available. See 
#'parameter 'lr_method' for more information. 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 lr_method a character vector indicating the linear regression method to be applied 
#'after the interpolation. Accepted methods are 'basic', 'large-scale' and '4nn'. The 'basic'
#'method fits a linear regression using high resolution observations as predictands and the 
#'interpolated model data as predictor. Then, the regression equation is to the interpolated 
#'model data to correct the interpolated values. The 'large-scale' method fits a linear
#'regression with large-scale predictors from the same model (e.g. teleconnection indices) 
#'as predictors and high-resolution observations as predictands. This equation is then 
#'applied to the interpolated model values. Finally, the '4nn' method uses a linear
#'regression with the four nearest neighbours as predictors and high-resolution observations 
#'as predictands. It is then applied to model data to correct the interpolated values.
#'@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 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 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 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".
#'@param predictors an array with large-scale data to be used in the 'large-scale' method.
#'Only needed if the linear regression method is set to 'large-scale'. It must have, at 
#'least the dimension start date and another dimension whose name has to be specified in 
#'the parameter 'large_scale_predictor_dimname'. It should contain as many elements as the
#'number of large-scale predictors.  
#'@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 time_dim a character vector indicating the time dimension name in the element
#''data' in exp and obs. Default set to "time". 
#'@param large_scale_predictor_dimname a character vector indicating the name of the 
#'dimension in 'predictors' that contain the predictor variables. See parameter 'predictors'.
#'@param loocv a logical indicating whether to apply leave-one-out cross-validation when
#'generating the linear regressions. Default to TRUE.
#'@param region a numeric vector indicating the borders of the region defined in exp.
#'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 multiApply 
#'
#'@return A 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) 
#'exp <- s2dv_cube(data = exp, lat = exp_lats, lon = exp_lons)
#'obs <- s2dv_cube(data = obs, lat = obs_lats, lon = obs_lons)
#'res <- CST_Intlr(exp = exp, obs = obs, target_grid = 'r1280x640', lr_method = 'basic', int_method = 'conservative')
#'@export
CST_Intlr <- function(exp, obs, lr_method, target_grid = NULL, points = NULL, int_method = NULL, 
                      method_point_interp = NULL, predictors = NULL, lat_dim = "lat", lon_dim = "lon", 
                      sdate_dim = "sdate", time_dim = "time", member_dim = "member",
                      large_scale_predictor_dimname = 'vars', loocv = TRUE, 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 <- Intlr(exp = exp$data, obs = obs$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]], points = points, 
               source_file_exp = exp$attrs$source_files[1], source_file_obs = obs$attrs$source_files[1], 
               target_grid = target_grid, lr_method = lr_method, int_method = int_method, 
               method_point_interp = method_point_interp, predictors = predictors, 
               lat_dim = lat_dim, lon_dim = lon_dim, sdate_dim = sdate_dim, time_dim = time_dim, 
               member_dim = member_dim, large_scale_predictor_dimname = large_scale_predictor_dimname, 
               loocv = loocv, region = region, ncores = ncores) 
  
  # Modify data, lat and lon in the origina s2dv_cube, adding the downscaled data
  exp$data <- res$data
  exp$dims <- dim(exp$data)
  exp$coords[[lon_dim]] <- res$lon
  exp$coords[[lat_dim]] <- res$lat
  
  obs$data <- res$obs
  obs$dims <- dim(obs$data)
  obs$coords[[lon_dim]] <- res$lon
  obs$coords[[lat_dim]] <- res$lat
  
  res_s2dv <- list(exp = exp, obs = obs)
  return(res_s2dv)
}

#'@rdname Intlr
#'@title Downscaling using interpolation and linear regression.
#' 
#'@author J. Ramon, \email{jaume.ramon@bsc.es}
#'
#'@description This function performs a downscaling using an interpolation and a linear 
#'regression. Different methodologies that employ linear regressions are available. See 
#'parameter 'lr_method' for more information. 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 and start date. 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_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 lr_method a character vector indicating the linear regression method to be applied 
#'after the interpolation. Accepted methods are 'basic', 'large-scale' and '4nn'. The 'basic'
#'method fits a linear regression using high resolution observations as predictands and the 
#'interpolated model data as predictor. Then, the regression equation is to the interpolated 
#'model data to correct the interpolated values. The 'large-scale' method fits a linear
#'regression with large-scale predictors from the same model (e.g. teleconnection indices) 
#'as predictors and high-resolution observations as predictands. This equation is then 
#'applied to the interpolated model values. Finally, the '4nn' method uses a linear
#'regression with the four nearest neighbours as predictors and high-resolution observations 
#'as predictands. It is then applied to model data to correct the interpolated values.
#'@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 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 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 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".
#'@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 predictors an array with large-scale data to be used in the 'large-scale' method.
#'Only needed if the linear regression method is set to 'large-scale'. It must have, at 
#'least the dimension start date and another dimension whose name has to be specified in 
#'the parameter 'large_scale_predictor_dimname'. It should contain as many elements as the
#'number of large-scale predictors.  
#'@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 time_dim a character vector indicating the time dimension name in the element
#''data' in exp and obs. Default set to "time". 
#'@param region a numeric vector indicating the borders of the region defined in exp.
#'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 large_scale_predictor_dimname a character vector indicating the name of the 
#'dimension in 'predictors' that contain the predictor variables. See parameter 'predictors'.
#'@param loocv a logical indicating whether to apply leave-one-out cross-validation when
#'generating the linear regressions. Default to TRUE.
#'@param ncores an integer indicating the number of cores to use in parallel computation.
#'The default value is NULL.
#'@import multiApply 
#'
#'@return A 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 <- Intlr(exp = exp, obs = obs, exp_lats = exp_lats, exp_lons = exp_lons, obs_lats = obs_lats, 
#'obs_lons = obs_lons, target_grid = 'r1280x640', lr_method = 'basic', int_method = 'conservative')
#'@export
Intlr <- function(exp, obs, exp_lats, exp_lons, obs_lats, obs_lons, lr_method, target_grid = NULL, points = NULL, 
                  int_method = NULL, method_point_interp = NULL, source_file_exp = NULL, source_file_obs = NULL, 
                  predictors = NULL, lat_dim = "lat", lon_dim = "lon", sdate_dim = "sdate", time_dim = "time", 
                  member_dim = "member", region = NULL, large_scale_predictor_dimname = 'vars', loocv = TRUE, 
                  ncores = NULL) {
  
  #-----------------------------------
  # Checkings
  #-----------------------------------
  if (!inherits(lr_method, 'character')) {
    stop("Parameter 'lr_method' must be of the class 'character'")
  }
  
  if (!inherits(large_scale_predictor_dimname, 'character')) {
    stop("Parameter 'large_scale_predictor_dimname' must be of the class 'character'")
  }
  
  if (!inherits(loocv, 'logical')) {
    stop("Parameter 'loocv' must be set to TRUE or FALSE")
  }
  
  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(large_scale_predictor_dimname, 'character')) {
    stop("Parameter 'large_scale_predictor_dimname' 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'")
  }

  # 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 for exp 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'.")
  }
  
  # sdate must be the time dimension in the input data
  stopifnot(sdate_dim %in% names(dim(exp)))
  stopifnot(sdate_dim %in% names(dim(obs))) 
  
  # the code is not yet prepared to handle members in the observations
  restore_ens <- FALSE
  if (member_dim %in% names(dim(obs))) {       
    if (identical(as.numeric(dim(obs)[member_dim]), 1)) {         
      obs <- ClimProjDiags::Subset(x = obs, along = member_dim, indices = 1, drop = 'selected')
      restore_ens <- TRUE       
    } else {         
      stop("Not implemented for observations with members ('obs' can have 'member_dim', ",
           "but it should be of length = 1).")       
    }     
  }
  
  # checkings for the parametre 'predictors' 
  if (!is.null(predictors)) {
    if (!is.array(predictors)) {
      stop("Parameter 'predictors' must be of the class 'array'")
    } else {
      # ensure the predictor variable name matches the parametre large_scale_predictor_dimname
      stopifnot(large_scale_predictor_dimname %in% names(dim(predictors)))
      stopifnot(sdate_dim %in% names(dim(predictors)))
      stopifnot(dim(predictors)[sdate_dim] == dim(exp)[sdate_dim]) 
    }
  }
  ## 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.")
    }
  }
  
  #-----------------------------------
  # Interpolation
  #-----------------------------------
  if (lr_method != '4nn') {
    
    if (is.null(int_method)) {
      stop("Parameter 'int_method' must be a character vector indicating the interpolation method. ",
           "Accepted methods are con, bil, bic, nn, con2")
    }
    
    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)])
    }
    
    exp_interpolated <- Interpolation(exp = exp, lats = exp_lats, lons = exp_lons, target_grid = target_grid, 
                                      points = points, method_point_interp = method_point_interp, 
                                      source_file = source_file_exp, lat_dim = lat_dim, lon_dim = lon_dim, 
                                      method_remap = int_method, 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, 
                                        points = points, method_point_interp = method_point_interp, 
                                        source_file = source_file_obs, lat_dim = lat_dim, lon_dim = lon_dim, 
                                        method_remap = int_method, region = region, ncores = ncores)
      
      lats <- obs_interpolated$lat
      lons <- obs_interpolated$lon 
      obs_interpolated <- obs_interpolated$data
    } else {
      obs_interpolated <- obs
      lats <- obs_lats
      lons <- obs_lons
    }
  }
  
  #-----------------------------------
  # Linear regressions
  #-----------------------------------
  # Pointwise linear regression
  # Predictor: model data
  # Predictand: observations
  if (lr_method == 'basic') {
    predictor <- exp_interpolated$data
    predictand <- obs_interpolated
    
    target_dims_predictor <- sdate_dim
    target_dims_predictand <- sdate_dim
  } 
  
  # (Multi) linear regression with large-scale predictors
  # Predictor: passed through the parameter 'predictors' by the user. Can be model or observations
  # Predictand: model data
  else if (lr_method == 'large-scale') {  
    if (is.null(predictors)) {
      stop("The large-scale predictors must be passed through the parametre 'predictors'")
    }  
    
    predictand <- obs_interpolated
    predictor <- predictors
    
    target_dims_predictor <- c(sdate_dim, large_scale_predictor_dimname)
    target_dims_predictand <- sdate_dim
  } 
  
  # Multi-linear regression with the four nearest neighbours
  # Predictors: model data
  # Predictand: observations
  else if (lr_method == '4nn') {
    
    predictor <- .find_nn(coar = exp, lats_hres = obs_lats, lons_hres = obs_lons, lats_coar = exp_lats, 
                         lons_coar = exp_lons, lat_dim = lat_dim, lon_dim = lon_dim, nn = 4, ncores = ncores)
    
    if (is.null(points) | ("location" %in% names(dim(obs)))) {
      if (!is.null(target_grid)) {
        warning("Interpolating to the 'obs' grid")
      }
      predictand <- obs
      
      lats <- obs_lats
      lons <- obs_lons
    }
    # If the downscaling is to point locations: Once the 4 nearest neighbours have been found, 
    # interpolate to point locations
    else {
      predictor <- Interpolation(exp = predictor, lats = obs_lats, lons = obs_lons, target_grid = NULL,
                                 points = points, method_point_interp = method_point_interp, 
                                 source_file = source_file_obs, method_remap = NULL, region = region, ncores = ncores)
      
      predictand <- Interpolation(exp = obs, lats = obs_lats, lons = obs_lons, target_grid = NULL,
                                  points = points, method_point_interp = method_point_interp, 
                                  source_file = source_file_obs, method_remap = NULL, region = region, ncores = ncores)
      
      lats <- predictor$lat
      lons <- predictor$lon 
      predictor <- predictor$data
      predictand <- predictand$data
    }
    
    target_dims_predictor <- c(sdate_dim,'nn')
    target_dims_predictand <- sdate_dim
  }
  
  else {
    stop(paste0(lr_method, " method is not implemented yet"))
  }

  print(paste0('dim predictor',dim(predictor)))
  print(paste0('dim predictand',dim(predictand)))
  print(dim(list(predictor[1])))
  # Apply the linear regressions
  
 
  
  res <- Apply(list(predictor, predictand), target_dims = list(target_dims_predictor, target_dims_predictand), 
               fun = .intlr, loocv = loocv, ncores = ncores)$output1
  
  names(dim(res))[1] <- sdate_dim
  # names(dim(res))[which(names(dim(res)) == '')]
  
  # restore ensemble dimension in observations if it existed originally
  if (restore_ens) {
    predictand <- s2dv::InsertDim(predictand, posdim = 1, lendim = 1, name = member_dim)
  }
  
  # Return a list of three elements
  res <- list(data = res, obs = predictand, lon = lons, lat = lats)
  
  return(res)
}

#-----------------------------------
# Atomic function to generate and apply the linear regressions
#-----------------------------------
.intlr <- function(x, y, loocv) {
  
  tmp_df <- data.frame(x = x, y = y)  
  # if the data is all NA, force return return NA
  if (all(is.na(tmp_df)) | (sum(apply(tmp_df, 2, function(x) !all(is.na(x)))) == 1)) {
    
    n <- nrow(tmp_df)
    res <- rep(NA, n)
    
  } else {
    # training
    lm1 <- .train_lm(df = tmp_df, loocv = loocv)  
    
    # prediction
    res <- .pred_lm(lm1 = lm1, df = tmp_df, loocv = loocv)
  }
  
  return(res)
}

#-----------------------------------
# Function to generate the linear regressions.
# Returns a list
#-----------------------------------
.train_lm <- function(df, loocv) {
  
  # Remove predictor columns containing only NA's
  df <- df[ ,apply(as.matrix(df[,colnames(df) != 'y'],nrow(df),ncol(df)-1), 2, function(x) !all(is.na(x)))]
  
  if (loocv) {
    
    lm1 <- lapply(1:nrow(df), function(j) {
      if (all(is.na(df[-j,]$y))) {
        return(NA)
      } else {
        return(lm(df[-j,], formula = y ~ .)) 
      }})
  } else {
    
    lm1 <- ifelse(all(is.na(df$y)), NA, list(lm(data = df, formula = y ~ .)))
  }
  
  return(lm1)
}

#-----------------------------------
# Function to apply the linear regressions.
#-----------------------------------
.pred_lm <- function(df, lm1, loocv) {
  
  if (loocv) {
    pred_vals <- sapply(1:nrow(df), function(j) {
      if (all(is.na(lm1[[j]]))) {
        return(NA)
      } else {
        return(predict(lm1[[j]], df[j,]))
      }})
  } else {
    if (!is.na(lm1)) { 
      pred_vals_ls <- lapply(lm1, predict, data = df)    
      pred_vals <- unlist(pred_vals_ls)
    } else {
      pred_vals <- rep(NA, nrow(df))
    }
  } 
  return(pred_vals)
}

#-----------------------------------
# Function to find N nearest neighbours.
# 'coar' is an array with named dimensions
#-----------------------------------
.find_nn <- function(coar, lats_hres, lons_hres, lats_coar, lons_coar, lat_dim, lon_dim, nn = 4, ncores = NULL) {
  
  # Sort the distances from closest to furthest
  idx_lat <- as.array(sapply(lats_hres, function(x) order(abs(lats_coar - x))[1:nn]))
  idx_lon <- as.array(sapply(lons_hres, function(x) order(abs(lons_coar - x))[1:nn]))
  
  names(dim(idx_lat)) <- c('nn', lat_dim)
  names(dim(idx_lon)) <- c('nn', lon_dim)
  
  # obtain the values of the nearest neighbours 
  nearest <- Apply(list(coar, idx_lat, idx_lon), 
                   target_dims = list(c(lat_dim, lon_dim), lat_dim, lon_dim), 
                   fun = function(x, y, z) x[y, z], ncores = ncores)$output1
  
  return(nearest)
}
