#'@rdname CST_Interpolation
#'@title Regrid or interpolate gridded data to a point location.
#'
#'@author J. Ramon, \email{jaume.ramon@bsc.es}
#'
#'@description This function interpolates gridded model data from one grid to
#'another (regrid) or interpolates gridded model data to a set of point locations.
#'The gridded model data can be either global or regional. In the latter case, the
#'region is defined by the user. It does not have constrains of specific region or
#'variables to downscale.
#'@param exp 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 and longitude. The field data 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 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_remap 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 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 lat_dim a character vector indicating the latitude dimension name in the element
#''exp' and/or 'points'. Default set to "lat".
#'@param lon_dim a character vector indicating the longitude dimension name in the element
#''exp' and/or 'points'. Default set to "lon".
#'@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 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 ncores an integer indicating the number of cores to use in parallel computation. 
#'The default value is NULL.
#'@seealso \code{\link[s2dverification]{CDORemap}}
#'
#'@return An s2dv object containing the dowscaled field.
#'
#'@examples
#'exp <- rnorm(500)
#'dim(exp) <- c(member = 5, lat = 4, lon = 5, sdate = 5, time  = 1)
#'lons <- 1:5
#'lats <- 1:4
#'exp <- s2dv_cube(data = exp, lat = lats, lon = lons) 
#'res <- CST_Interpolation(exp = exp, method_remap = 'conservative', target_grid = 'r1280x640')
#'@export
CST_Interpolation <- function(exp, points = NULL, method_remap = NULL, target_grid = NULL, 
                              lat_dim = "lat", lon_dim = "lon", region = NULL, 
                              method_point_interp = NULL, ncores = NULL)
{
  if (!inherits(exp,'s2dv_cube')) {
    stop("Parameter 'exp' must be of the class 's2dv_cube'")
  }

  #if (is.null(exp[[lat_dim]]) | is.null(exp[[lon_dim]])) {
  #  stop("The name of the latitude/longitude elements in 'exp' must match the parametres ",
  #       "'lat_dim' and 'lon_dim'")
  #}
  
  if ((length(which(names(dim(exp$data)) == lat_dim)) == 0) | (length(which(names(dim(exp$data)) == lon_dim)) == 0)) {
    stop("The name of the latitude/longitude dimensions in 'exp$data' must match the parametres 'lat_dim' and 'lon_dim'")
  }

  res <- Interpolation(exp = exp$data, lats = exp$coords[[lat_dim]], lons = exp$coords[[lon_dim]], 
                       source_file = exp$coords$source_files[1], points = points,
                       method_remap = method_remap, target_grid = target_grid, lat_dim = lat_dim,
                       lon_dim = lon_dim, region = region, method_point_interp = method_point_interp, 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

  res_s2dv <- list(exp = exp, obs  = NULL)
  return(res_s2dv)
}

#'@rdname Interpolation
#'@title Regrid or interpolate gridded data to a point location.
#' 
#'@author J. Ramon, \email{jaume.ramon@bsc.es}
#'@author Ll. Lledó, \email{llorenc.lledo@ecmwf.int}
#'
#'@description This function interpolates gridded model data from one grid to  
#'another (regrid) or interpolates gridded model data to a set of point locations.
#'The gridded model data can be either global or regional. In the latter case, the 
#'region is defined by the user. It does not have constrains of specific region or 
#'variables to downscale.
#'@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 and longitude. 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 lats a numeric vector containing the latitude values. Latitudes must range from 
#'-90 to 90.
#'@param lons a numeric vector containing the longitude values. Longitudes can range from
#'-180 to 180 or from 0 to 360.
#'@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 source_file 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 method_remap 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 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 lat_dim a character vector indicating the latitude dimension name in the element 
#''exp' and/or 'points'. Default set to "lat".
#'@param lon_dim a character vector indicating the longitude dimension name in the element
#''exp' and/or 'points'. Default set to "lon".
#'@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 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 ncores an integer indicating the number of cores to use in parallel computation. 
#'The default value is NULL.
#'@import multiApply
#'@import plyr 
#'@importFrom s2dv CDORemap
#'
#'@seealso \code{\link[s2dverification]{CDORemap}}
#'
#'@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, time  = 1) 
#'lons <- 1:5 
#'lats <- 1:4 
#'res <- Interpolation(exp = exp, lats = lats, lons = lons, method_remap = 'conservative', target_grid = 'r1280x640')
#'@export
Interpolation <- function(exp, lats, lons, points = NULL, source_file = NULL, method_remap = NULL,  
                          target_grid = NULL, lat_dim = "lat", lon_dim = "lon", region = NULL, 
                          method_point_interp = NULL, ncores = NULL) 
{
  if (!is.null(method_remap)) {
    if (!inherits(method_remap, 'character')) {
      stop("Parameter 'method_remap' must be of the class 'character'")
    }
  }
 
  if (!is.null(method_point_interp)) {
    if (!inherits(method_point_interp, 'character')) {
      stop("Parameter 'method_point_interp' 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'")
  }

  # Check for negative latitudes in the exp data
  if (any(lats < -90 | lats > 90)  ) {
      stop("Out-of-range latitudes have been found. Latitudes must range from -90 to 90")
  }
 
  # checkings for the case of interpolation to point locations
  if (!is.null(points)) {
    if (!inherits(points, 'list')) {
      stop("Parameter 'points' must be a list of two elements containing the point ",
           "latitudes and longitudes.")
    }

    if (is.null(method_point_interp)) {
      stop("Parameter 'method_point_interp' must be a character vector indicating the ",
           "interpolation method. Accepted methods are nearest, bilinear, 9point, ",
           "invdist4nn, NE, NW, SE, SW")
    }

    if (!(method_point_interp %in% c('nearest', 'bilinear', '9point', 'invdist4nn', 'NE', 'NW', 'SE', 'SW'))) {
      stop("Parameter 'method_point_interp' must be a character vector indicating the ",
           "interpolation method. Accepted methods are nearest, bilinear, 9point, ",
           "invdist4nn, NE, NW, SE, SW")
    }
 
    # Points must be a list of two elements
    if (length(points) != 2) {
      stop("'points' must be a lis of two elements containing the point ",
           "latitudes and longitudes in the form 'points$lat', 'points$lon'")
    }

    # The names of the two elements must be 'lat' and 'lon'
    if (any(!(c(lat_dim, lon_dim) %in% names(points)))) {
      stop("The names of the elements in the list 'points' must coincide with the parametres ", 
           "'lat_dim' and 'lon_dim'")
    }

    # Check that the number of latitudes and longitudes match
    if (length(unique(lengths(points))) != 1L) {
      stop("The number of latitudes and longitudes must match")
    }

    # Check for negative latitudes in the point coordinates
    if (any(points[[lat_dim]] < -90 | points[[lat_dim]] > 90)  ) {
      stop("Out-of-range latitudes have been found in 'points'. Latitudes must range from ",
           "-90 to 90")
    }
    
    if (is.null(source_file)) {
        stop("No source file found. Source file must be provided in the parameter 'source_file'.")
    }
  } else {
    if (is.null(method_remap)) {
      stop("Parameter 'method_remap' must be a character vector indicating the ",
           "interpolation method. Accepted methods are con, bil, bic, nn, con2")
    }

    if (is.null(target_grid)) {
      stop("Parameter 'target_grid' can be either a path ",
           "to another NetCDF file which to read the target grid from (a single grid must be ",
           "defined in such file) or a character vector indicating the coarse grid to ",
           "be passed to CDO, and it must be a grid recognised by CDO or a NetCDF file.")
    }
  }
  ## 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.")
    }
  }
  
  #----------------------------------
  # Limits of the region defined by the model data
  #----------------------------------
  # for the case when region limits are not passed by the user
  # regions contains the following elements in order: lonmin, lonmax, latmin, latmax
  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 'lats' and 'lons'.")
    region <- c(lons[1], lons[length(lons)], lats[1], lats[length(lats)])
  }
  
  # Ensure points to be within the region limits
  if (!is.null(points)) {
    if (any(points[[lat_dim]] > region[4]) | any(points[[lat_dim]] < region[3]) | 
        any(points[[lon_dim]] > region[2]) | any(points[[lon_dim]] < region[1])) {
      stop("At least one of the points lies outside the model region")
    }
  }

  #----------------------------------
  # Map regrid with CDO
  #----------------------------------
  if (is.null(points)) {
    
    .KnownLonNames <- s2dv:::.KnownLonNames
    .KnownLatNames <- s2dv:::.KnownLatNames
    .warning <- s2dv:::.warning
    
    res <- CDORemap(data_array = exp,
                    lats = lats,
                    lons = lons,
                    grid = target_grid,
                    method = method_remap,
                    crop = region)

    # Return a list
    res <- list(data = res$data_array, obs = NULL, lon = res$lons, lat = res$lats)

  #----------------------------------
  # Interpolate to point locations
  #----------------------------------
  } else {  
    # First create interpolation weights, depending on the chosen method
    weights <- .create_interp_weights(ncfile = source_file, locids = 1:unique(lengths(points)),
                                     lats = points[[lat_dim]], lons = points[[lon_dim]], 
                                     method = method_point_interp, region = list(lat_min = region[3], 
                                     lat_max = region[4], lon_min = region[1], lon_max = region[2])) 

    # Select coarse-scale data to be interpolated
    model_data_gridpoints <- .get_model_data(weights.df = weights, mdata = exp, ncores = ncores)

    # Interpolate model data to point locations
    res <- .interpolate_data(model_data_gridpoints, weights, ncores = ncores)
  
    # Return a list
    res <- list(data = res, obs = NULL, lon = points[[lon_dim]], lat = points[[lat_dim]])
  }

  return(res)
}

#======================
# Compute weights for interpolation at several (lat,lon) positions
# We assume that grid boxes are centered in the grid point.
#======================
.create_interp_weights <- function(ncfile, locids, lats, lons, region = NULL,
                                  method = c("nearest", "bilinear", "9point", "invdist4nn", "NE", 
                                             "NW", "SE", "SW")) 
{ 
  # crop the region to get the correct weights - save temporary file
  nc_cropped1 <- paste0('tmp_cropped_', format(Sys.time(), "%Y%m%d%H%M"),'.nc')
  nc_cropped2 <- paste0('tmp_cropped2_', format(Sys.time(), "%Y%m%d%H%M"),'.nc')

  system(paste0('cdo sellonlatbox,', region$lon_min, ',', region$lon_max, ',', region$lat_min,
         ',', region$lat_max, ' ', ncfile, ' ', nc_cropped1))
  
  #----------------
  # Read grid description and compute (i,j) of requested locations (including decimals)
  #----------------
  griddes <- .get_griddes(nc_cropped1)

  if (is.null(griddes$yinc)) {
    system(paste0('rm ', nc_cropped1))
    stop("'griddes$yinc' not found in NetCDF file. Remember that only regular grids are accepted when ",
         "downscaling to point locations.")
  }

  # If latitudes are decreasingly ordered, revert them
  if (griddes$yinc < 0) {
    system(paste0('cdo invertlat ', nc_cropped1, ' ', nc_cropped2))
    griddes <- .get_griddes(nc_cropped2)
  }
  # remove temporary files
  system(paste0('rm ', nc_cropped1))
  system(paste0('rm ', nc_cropped2))
  
  if (is.null(griddes)) {
    stop("'griddes' not found in the NetCDF source files")
  } 
  
  gridpoints <- .latlon2ij(griddes, lats, lons)

  #----------------
  # Compute the weights according to the selected method
  #----------------
  if(method == "nearest") {
    #----------------
    # Round i and j to closest integer. Weight is always 1.
    #----------------

    #  |     |     |
    # -+-----+-----+-
    #  |    x|     |
    #  |  a  |     |
    #  |     |     |
    # -+-----+-----+-
    #  |     |     |

    centeri <- round(gridpoints$i,0)
    centeri[centeri == griddes$xsize+1] <- 1 # close longitudes		

    weights.df <- data.frame(locid = locids,
                             lat = lats,
                             lon = lons,
                             rawi = gridpoints$i,
                             rawj = gridpoints$j,
                             i = centeri,
                             j = round(gridpoints$j, 0),
                             weight = 1)
  } else if (method %in% c("bilinear","invdist4nn")) {
    #----------------
    # Get the (i,j) coordinates of the 4 points (a,b,c,d) around x.
    # This plot shows i increasing to the right and 
    # j increasing to the top, but the computations are generic.
    #----------------
    #  |     |     |
    #- +-----+-----+-
    #  |     |     |
    #  |  b  |  c  |
    #  |     |     |
    #- +-----+-----+-
    #  |    x|     |
    #  |  a  |  d  |
    #  |     |     |
    #- +-----+-----+-
    #  |     |     |

    lowi <- floor(gridpoints$i)
    highi <- ceiling(gridpoints$i)
    highi[highi == griddes$xsize+1] <- 1  # close the longitudes
    lowj <- floor(gridpoints$j)
    highj <- ceiling(gridpoints$j)
    # Note: highi and lowi are the same if i is integer
    # Note: highj and lowj are the same if j is integer

    #----------------
    # Get x position wrt ad and ab axes (from 0 to 1)
    #----------------
    pcti <- gridpoints$i - lowi
    pctj <- gridpoints$j - lowj

    #----------------
    # Compute weights for a,b,c,d grid points
    #----------------
    if(method == "bilinear") {
      wa = (1 - pcti) * (1 - pctj)
      wb = (1 - pcti) * pctj
      wc = pcti * pctj
      wd = pcti * (1 - pctj)
    } else if(method == "invdist4nn") {
      #----------------
      # Note: the distance is computed in the (i,j) space.
      # Note2: this method does not guarantees a continuous interpolation.
      # Use bilinear if that's desirable. 
      # When x is on the ab line, c and d would be used. In the limit of x 
      # being just left of ab other points would be used.
      # Here we just dropped c and d coeffs when over ab. Same for ad line, 
      # b and c coeffs dropped. This prevents repeated nodes.
      #----------------
      ida = 1 / sqrt(pcti^2 + pctj^2)
      idb = 1 / sqrt(pcti^2 + (1 - pctj)^2)
      idc = 1 / sqrt((1-pcti)^2 + (1-pctj)^2)
      idd = 1 / sqrt((1-pcti)^2 + pctj^2)
      idb[pctj == 0] <- 0; 
      idc[pctj == 0] <- 0; 
      idc[pcti == 0] <- 0;
      idd[pcti == 0] <- 0;
 
      #----------------
      # Normalize vector of inverse distances
      #----------------
      invdist <- cbind(ida, idb, idc, idd)
      print(invdist)
      w <- t(apply(invdist, 1, function(x) { print(x); if(any(is.infinite(x))) {
                   x <- is.infinite(x) * 1 } ; x <- x/sum(x) }))
      print(w)

      wa = w[ , 1]
      wb = w[ , 2]
      wc = w[ , 3]
      wd = w[ , 4]
    }

    #----------------
    # Put info in dataframes and rbind them
    #----------------
    weightsa.df <- data.frame(locid = locids, lat = lats,lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = lowi, j = lowj, weight = wa)
    weightsb.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = lowi, j = highj, weight = wb)
    weightsc.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = highi, j = highj, weight = wc)
    weightsd.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = highi, j = lowj, weight = wd)
    weights.df <- rbind(weightsa.df, weightsb.df, weightsc.df, weightsd.df)
  } else if(method == "9point") {
    #----------------
    # Get the (i,j) coordinates of the 9 points (a,b,...,i) around x
    # This plot shows i increasing to the right and 
    # j increasing to the top, but the computations are generic.
    #----------------
    # |     |     |     |
    #-+-----+-----+-----+-
    # |     |     |     |
    # |  c  |  f  |  i  |
    # |     |     |     |
    #-+-----+-----+-----+-
    # |     |    x|     |
    # |  b  |  e  |  h  |
    # |     |     |     |
    #-+-----+-----+-----+-
    # |     |     |     |
    # |  a  |  d  |  g  |
    # |     |     |     |
    #-+-----+-----+-----+-
    # |     |     |     |

    centeri <- round(gridpoints$i, 0)
    centeri[centeri == griddes$xsize + 1] <- 1 
    centerj <- round(gridpoints$j, 0)
    lowi <- centeri - 1
    highi <- centeri + 1
    lowi[lowi == 0] <- griddes$xsize # close the longitudes
    highi[highi == griddes$xsize+1] <- 1 # close the longitudes
    lowj <- centerj - 1
    highj <- centerj + 1
		
    #----------------
    # For the north and south pole do a 6-point average
    #----------------
    w_highj   <- ifelse(centerj == 1,1/6,ifelse(centerj == griddes$ysize,0  ,1/9))
    w_centerj <- ifelse(centerj == 1,1/6,ifelse(centerj == griddes$ysize,1/6,1/9))
    w_lowj    <- ifelse(centerj == 1,0  ,ifelse(centerj == griddes$ysize,1/6,1/9))

    #----------------
    # Put info in dataframes and rbind them
    #----------------
    weightsa.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = lowi, j = lowj, weight = w_lowj)
    weightsb.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = lowi, j = centerj, weight = w_centerj)
    weightsc.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = lowi, j = highj, weight = w_highj)
    weightsd.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = centeri, j = lowj, weight = w_lowj)
    weightse.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = centeri, j = centerj, weight = w_centerj)
    weightsf.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = centeri, j = highj, weight = w_highj)
    weightsg.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = highi, j = lowj, weight = w_lowj)
    weightsh.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = highi, j = centerj, weight = w_centerj)
    weightsi.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i,
                              rawj = gridpoints$j, i = highi, j = highj, weight = w_highj)
    weights.df <- rbind(weightsa.df, weightsb.df, weightsc.df, weightsd.df, weightse.df,
                        weightsf.df, weightsg.df, weightsh.df, weightsi.df)
  } else if(method %in% c("NE", "NW", "SW", "SE")) {
    #----------------
    # Find if increasing i and j increases or decreases lat and lon
    #----------------
    westtoeast <- (griddes$xinc > 0)
    southtonorth <- T
    if(griddes$gridtype == "gaussian") { 
      # We assume gaussian grid latitudes are ordered north to south
      southtonorth <- F 
      } else { #lonlat
        if(griddes$yinc < 0) {southtonorth <- F}
      }

      #----------------
      # Get the (i,j) coordinates of the desired point (a,b,c or d) around x
      #----------------
      #  |     |     |
      #- +-----+-----+-
      #  |     |     |
      #  |  b  |  c  |
      #  |     |     |
      #- +-----+-----+-
      #  |    x|     |
      #  |  a  |  d  |
      #  |     |     |
      #- +-----+-----+-
      #  |     |     |

      if(substr(method,1,1) == "N" & southtonorth == T) { selj <- ceiling(gridpoints$j) }
      if(substr(method,1,1) == "S" & southtonorth == T) { selj <- floor(gridpoints$j) }
      if(substr(method,1,1) == "N" & southtonorth == F) { selj <- floor(gridpoints$j) }
      if(substr(method,1,1) == "S" & southtonorth == F) { selj <- ceiling(gridpoints$j) }

      if(substr(method,2,2) == "E" & westtoeast == T) {seli <- ceiling(gridpoints$i) }
      if(substr(method,2,2) == "W" & westtoeast == T) {seli <- floor(gridpoints$i) }
      if(substr(method,2,2) == "E" & westtoeast == F) {seli <- floor(gridpoints$i) }
      if(substr(method,2,2) == "W" & westtoeast == F) {seli <- ceiling(gridpoints$i) }
      
      seli[seli == griddes$xsize + 1] <- 1  # close the longitudes
      
      weights.df <- data.frame(locid = locids, lat = lats, lon = lons, rawi = gridpoints$i, 
                               rawj = gridpoints$j, i = seli, j = selj, weight = 1)
  } else { 
    stop(paste0("Method " ,method, " not implemented"))
  }
  
  #----------------  
  # Order by locid and remove lines with 0 weight 
  # This also removes some duplicates in the bilinear/invdist4nn methods when i 
  # or j is a whole number, or in the 9-point method when at the poles.
  #----------------
  weights.df <- weights.df[order(weights.df$locid), ]
  weights.df <- weights.df[weights.df$weight != 0, ]
	
  #----------------
  # Add as attributes the method and the nc file used to compute the weights
  #----------------
  attributes(weights.df)$nc_file <- normalizePath(ncfile)
  attributes(weights.df)$method <- method

  return(weights.df)
}

#======================
# Compute (i,j) from (lat,lon).
# Works only for 'lonlat' and 'gaussian' grids.
# Grids are supposed to cover whole globe.
#======================
.latlon2ij <- function(griddes, lats, lons) {
  #------------
  # Check input params
  #------------
  if(length(lons) != length(lats)) {stop("Input lat and lon have different lengths.")}
  if(any(lats < -90) | any(lats > 90)) {stop("Latitude out of valid range")}
  if((griddes$xfirst > 180) & (any(lons < 0))) {
    stop("Please use the same convention for the longitudes in the source file and the ",
         "longitude values in 'points'.")
  }
  #if(round(griddes$xinc*griddes$xsize) != 360) {stop("Grid is not global")}
  # no need to resize lons to [0,360)

  #------------
  # Compute i (with decimals)
  # i lies in [1,xsize+1)
  # %% gives the remainder
  #------------
  gridpoints <- list()
  gridpoints$i <- 1 + (((lons - griddes$xfirst) / griddes$xinc) %% griddes$xsize)
 
  #------------
  # Compute j (with decimals)
  #------------
  if(griddes$gridtype=='lonlat') {
    gridpoints$j <- 1 + (lats - griddes$yfirst) / griddes$yinc 
  } else if(griddes$gridtype == 'gaussian') {
    # We assume gaussian grid latitudes are ordered north to south
    # findInterval can only work with monotonic ascending values so we revert twice
    northj <- griddes$ysize-findInterval(lats, -griddes$yvals)
    southj <- northj + 1

    # Special case: We are north of the first lat
    gridpoints$j[northj == 0] <- 1

    # Special case: We are south of the last lat
    gridpoints$j[southj == griddes$ysize + 1] <- griddes$ysize

    # Generic case
    ok_idx <- !(northj == 0 | southj == griddes$ysize+1)
    gridpoints$j[ok_idx] <- northj[ok_idx] + (griddes$yvals[northj[ok_idx]] - 
                            lats[ok_idx])/(griddes$yvals[northj[ok_idx]] - griddes$yvals[southj[ok_idx]])
  } else { stop("Unsupported grid") }
	
  return(gridpoints)
}

#======================
# Use cdo griddes to obtain grid information
#======================
.get_griddes <- function(ncfile) {
  tmp <- system(paste0("cdo griddes ", ncfile,
                       " 2>/dev/null | egrep 'gridtype|xsize|ysize|xfirst|xinc|yfirst|yinc'"), intern = T)
  arr <- do.call(rbind, strsplit(tmp,"\\s+= ", perl = T))
  griddes <- as.list(arr[,2])
  names(griddes) <- arr[,1]

  if(griddes$gridtype == "gaussian") {
    griddes$yvals <- .get_lats(ncfile)
  }

  # Convert some fields to numeric. Ensures all fields are present.
  for(nm in c("xsize", "ysize", "xfirst", "yfirst", "xinc", "yinc")) {
    griddes[[nm]] <- ifelse(is.null(griddes[[nm]]), NA, as.numeric(griddes[[nm]]))
  }
	
  return(griddes)
}

#======================
# Use nco to obtain latitudes. Latitudes shall be named "lat" or "latitude".
#======================
.get_lats <- function(ncfile) {

  tmp <- system(paste0('ncks -H -s "%f " -v latitude ',ncfile),intern=T)

  if(!is.null(attributes(tmp)$status)) {
    tmp <- system(paste0('ncks -H -s "%f " -v lat ',ncfile),intern=T)
  }
  
  lats <- as.numeric(strsplit(tmp[1],"\\s+",perl=T)[[1]])

  return(lats)
}

#======================
# Load model data at all (i,j) pairs listed in the weights dataframe. 
# Uses StartR. All ... parameters go to Start (i.e. specify dat, var, 
# sdate, time, ensemble, num_procs, etc)
#======================
.get_model_data <- function(weights.df, mdata, ncores = NULL) {
 
  #-----------------
  # Get data for all combinations of i and j.
  # (inefficient, getting many unneded pairs).
  # Avoid retrieving duplicates with unique()
  # These are the indices of the global grid
  #-----------------
  is <- weights.df$i
  js <- weights.df$j

  #-----------------
  # If any of the indices happens to be 0,
  # change it by 1 but give a warning
  #-----------------
  if (any(is == 0) | any(js == 0)) {
    warning("Is the point location in the border of the region? The code can run but ",
            "results will be less accurate than those obtained with a larger region." )
    is[is == 0] <- 1
    js[js == 0] <- 1
  } 

  #-----------------
  # Get indices of original is and js in unique(is),unique(js) that were requested
  #-----------------
  idxi <- match(is, unique(is))
  idxj <- match(js, unique(js))

  #-----------------
  # Subsample mdata to keep only the needed (i,j) pairs.
  #-----------------
  if (is.na(match("longitude", names(dim(mdata))))) {
    londim <- match("lon", names(dim(mdata)))
  } else {
    londim <- match("longitude", names(dim(mdata)))
  }
  if (is.na(match("latitude", names(dim(mdata))))) {
    latdim <- match("lat", names(dim(mdata)))
  } else {
    latdim <- match("latitude", names(dim(mdata)))
  }

  # trick: exchange idxi and idxj
  #if(londim > latdim) { idx.tmp <- idxi; idxi <- idxj; idxj <- idx.tmp }
  #keepdims <- (1:length(dim(mdata)))[-c(londim,latdim)]

  #sub_mdata <- apply(mdata, keepdims, function(x) { 
  #                   laply(1:length(is),function(k) { x[idxi[k],idxj[k]] }) })
  #names(dim(sub_mdata))[1] <- "gridpoint"

  #-----------------
  # Retrieve with multiApply
  #-----------------
  sub_mdata <- Apply(mdata, target_dims = list(c(latdim, londim)), 
                     fun = function(x) {laply(1:length(is),function(k) { x[js[k],is[k]] }) }, 
                     ncores = ncores)$output1
  names(dim(sub_mdata))[1] <- "gridpoint"

  #-----------------
  # Return an array that contains as many gridpoints as (i,j) pairs were requested
  #-----------------
  return(sub_mdata)
}

#======================
# Multiply the grid-point series by the weights, 
# to obtain the desired interpolations
#======================
.interpolate_data <- function(model_data, weights.df, ncores) {
  #-----------------
  # Multiply each gridpoint matrix by its corresponding weight
  #-----------------
  gpdim <- match("gridpoint", names(dim(model_data)))
  weighted_data <- sweep(model_data, gpdim, weights.df$weight, "*")

  #-----------------
  # Sum all series that belong to same interpolation point
  # Return an array that contains the requested locations and interpolation type
  #-----------------
  #interp_data <- apply(weighted_data, -gpdim, function(x) { rowsum(x, weights.df$locid) }) 
  #names(dim(interp_data))[1] <- "location"
  interp_data <- Apply(weighted_data, target_dims = gpdim, fun = function(x) { 
    rowsum(x, weights.df$locid)}, output_dims = c("location", "aux"),
    ncores = ncores)$output1
  
  return(interp_data)
}
