#'Compute the North Atlantic Oscillation (NAO) Index
#'
#'Compute the North Atlantic Oscillation (NAO) index based on the leading EOF 
#'of the sea level pressure (SLP) anomalies over the north Atlantic region 
#'(20N-80N, 80W-40E). The PCs are obtained by projecting the forecast and 
#'observed anomalies onto the observed EOF pattern (Pobs) or the forecast 
#'anomalies onto the EOF pattern of the other years of the forecast (Pmod). 
#'By default (ftime_avg = 2:4) NAO() computes the NAO index for 1-month 
#'lead seasonal forecasts that can be plotted with PlotBoxWhisker(). It returns
#'cross-validated PCs of the NAO index for forecast (exp) and observations 
#'(obs) based on the leading EOF pattern.
#'
#'@param exp A named numeric array of North Atlantic SLP (20N-80N, 80W-40E) 
#'  forecast anomalies from \code{Ano()} or \code{Ano_CrossValid()} with 
#'  dimensions 'time_dim', 'memb_dim', 'ftime_dim', and 'space_dim' at least.
#'  If only NAO of observational data needs to be computed, this parameter can
#'  be left to NULL. The default value is NULL.
#'@param obs A named numeric array of North Atlantic SLP (20N-80N, 80W-40E) 
#'  observed anomalies from \code{Ano()} or \code{Ano_CrossValid()} with 
#'  dimensions 'time_dim', 'memb_dim', 'ftime_dim', and 'space_dim' at least.
#'  If only NAO of experimental data needs to be computed, this parameter can 
#'  be left to NULL. The default value is NULL.
#'@param lat A vector of the latitudes of 'exp' and 'obs'.
#'@param lon A vector of the longitudes of 'exp' and 'obs'.
#'@param time_dim A character string indicating the name of the time dimension
#' of 'exp' and 'obs'. The default value is 'sdate'. 
#'@param memb_dim A character string indicating the name of the member 
#'  dimension of 'exp' and 'obs'. The default value is 'member'.
#'@param space_dim A vector of two character strings. The first is the dimension
#'  name of latitude of 'ano' and the second is the dimension name of longitude
#'  of 'ano'. The default value is c('lat', 'lon').
#'@param ftime_dim A character string indicating the name of the forecast time 
#'  dimension of 'exp' and 'obs'. The default value is 'ftime'.
#'@param ftime_avg A numeric vector of the forecast time steps to average
#'  across the target period. The default value is 2:4, i.e., from 2nd to 4th 
#'  forecast time steps.
#'@param obsproj A logical value indicating whether to compute the NAO index by
#'  projecting the forecast anomalies onto the leading EOF of observational 
#'  reference (TRUE) or compute the NAO by first computing the leading 
#'  EOF of the forecast anomalies (in cross-validation mode, i.e. leaving the 
#'  year you are evaluating out), and then projecting forecast anomalies onto 
#'  this EOF (FALSE). The default value is TRUE.
#'@param ncores An integer indicating the number of cores to use for parallel 
#'  computation. The default value is NULL.
#'
#'@return 
#'A list which contains:
#'\item{exp}{
#'  A numeric array of forecast NAO index in verification format with the same 
#'  dimensions as 'exp' except space_dim and ftime_dim.
#'  }
#'\item{obs}{
#'  A numeric array of observed NAO index in verification format with the same
#'  dimensions as 'obs' except space_dim and ftime_dim.
#'}
#'
#'@references
#'Doblas-Reyes, F.J., Pavan, V. and Stephenson, D. (2003). The skill of 
#'  multi-model seasonal forecasts of the wintertime North Atlantic 
#'  Oscillation. Climate Dynamics, 21, 501-514. 
#'  DOI: 10.1007/s00382-003-0350-4
#'
#'@examples
#'  \dontshow{
#'startDates <- c('19851101', '19901101', '19951101', '20001101', '20051101')
#'sampleData <- s2dv:::.LoadSampleData('tos', c('experiment'),
#'                                     c('observation'), startDates,
#'                                     leadtimemin = 1,
#'                                     leadtimemax = 4,
#'                                     output = 'lonlat',
#'                                     latmin = 27, latmax = 48,
#'                                     lonmin = -12, lonmax = 40)
#'# No example data is available over NAO region, so in this example we will 
#'# tweak the available data. In a real use case, one can Load() the data over 
#'# the NAO region directly.
#'sampleData$lon[] <- c(40, 280, 340)
#'sampleData$lat[] <- c(20, 80)
#'  }
#'
#'# Now ready to compute the EOFs and project on, for example, the first 
#'# variability mode.
#'ano <- Ano_CrossValid(sampleData$mod, sampleData$obs)
#'# Note that computing the NAO over the region for which there is available 
#'# example data is not the full NAO area: NAO() will raise a warning.
#'nao <- NAO(ano$exp, ano$obs, sampleData$lat, sampleData$lon)
#'# Finally plot the NAO index
#'  \dontrun{
#'nao$exp <- Reorder(nao$exp, c(2, 1))
#'nao$obs <- Reorder(nao$obs, c(2, 1))
#'PlotBoxWhisker(nao$exp, nao$obs, "NAO index, DJF", "NAO index (PC1) TOS",
#'        monini = 12, yearini = 1985, freq = 1, "Exp. A", "Obs. X")
#'  }
#'
#'@import multiApply
#'@importFrom ClimProjDiags Subset
#'@export
NAO <- function(exp = NULL, obs = NULL, lat, lon, time_dim = 'sdate',
                memb_dim = 'member', space_dim = c('lat', 'lon'),
                ftime_dim = 'ftime', ftime_avg = 2:4, 
                obsproj = TRUE, ncores = NULL) {

  # Check inputs 
  ## exp and obs (1)
  if (is.null(obs) & is.null(exp)) {
    stop("Parameter 'exp' and 'obs' cannot both be NULL.")
  }
  if (!is.null(exp)) {
    if (!is.numeric(exp)) {
      stop("Parameter 'exp' must be a numeric array.")
    }
    if (is.null(dim(exp))) {
      stop(paste0("Parameter 'exp' and must have at least dimensions ",
                  "time_dim, memb_dim, space_dim, and ftime_dim."))
    }
    if(any(is.null(names(dim(exp)))) | any(nchar(names(dim(exp))) == 0)) {
      stop("Parameter 'exp' must have dimension names.")
    }
  }
  if (!is.null(obs)) {
    if (!is.numeric(obs)) {
      stop("Parameter 'obs' must be a numeric array.")
    }
    if (is.null(dim(obs))) {
      stop(paste0("Parameter 'obs' and must have at least dimensions ",
                  "time_dim, memb_dim, space_dim, and ftime_dim."))
    }
    if(any(is.null(names(dim(obs)))) | any(nchar(names(dim(obs))) == 0)) {
      stop("Parameter 'obs' must have dimension names.")
    }
  }
  ## time_dim
  if (!is.character(time_dim) | length(time_dim) > 1) {
    stop("Parameter 'time_dim' must be a character string.")
  }
  if (!is.null(exp)) {
    if (!time_dim %in% names(dim(exp))) {
      stop("Parameter 'time_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  if (!is.null(obs)) {
    if (!time_dim %in% names(dim(obs))) {
      stop("Parameter 'time_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  ## memb_dim
  if (!is.character(memb_dim) | length(memb_dim) > 1) {
    stop("Parameter 'memb_dim' must be a character string.")
  }
  if (!is.null(exp)) {
    if (!memb_dim %in% names(dim(exp))) {
      stop("Parameter 'memb_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  if (!is.null(obs)) {
    if (!memb_dim %in% names(dim(obs))) {
      stop("Parameter 'memb_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  ## space_dim
  if (!is.character(space_dim) | length(space_dim) != 2) {
    stop("Parameter 'space_dim' must be a character vector of 2.")
  }
  if (!is.null(exp)) {
    if (any(!space_dim %in% names(dim(exp)))) {
      stop("Parameter 'space_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  if (!is.null(obs)) {
    if (any(!space_dim %in% names(dim(obs)))) {
      stop("Parameter 'space_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  ## ftime_dim
  if (!is.character(ftime_dim) | length(ftime_dim) > 1) {
    stop("Parameter 'ftime_dim' must be a character string.")
  }
  if (!is.null(exp)) {
    if (!ftime_dim %in% names(dim(exp))) {
      stop("Parameter 'ftime_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  if (!is.null(obs)) {
    if (!ftime_dim %in% names(dim(obs))) {
      stop("Parameter 'ftime_dim' is not found in 'exp' or 'obs' dimension.")
    }
  }
  ## exp and obs (2)
  if (!is.null(exp) & !is.null(obs)) {
    name_exp <- sort(names(dim(exp)))
    name_obs <- sort(names(dim(obs)))
    name_exp <- name_exp[-which(name_exp == memb_dim)]
    name_obs <- name_obs[-which(name_obs == memb_dim)]
    if(!all(dim(exp)[name_exp] == dim(obs)[name_obs])) {
      stop(paste0("Parameter 'exp' and 'obs' must have the same length of ",
                  "all dimensions except 'memb_dim'."))
    }
  }
  ## ftime_avg
  if (!is.vector(ftime_avg) | !is.integer(ftime_avg)) {
    stop("Parameter 'ftime_avg' must be an integer vector.")
  }
  if (!is.null(exp)) {
    if (max(ftime_avg) > dim(exp)[ftime_dim] | min(ftime_avg) < 1) {
      stop("Parameter 'ftime_avg' must be within the range of ftime_dim length.")
    }
  } else {
    if (max(ftime_avg) > dim(obs)[ftime_dim] | min(ftime_avg) < 1) {
      stop("Parameter 'ftime_avg' must be within the range of ftime_dim length.")
    }
  }
  ## sdate >= 2
  if (!is.null(exp)) {
    if (dim(exp)[time_dim] < 2) {
      stop("The length of time_dim must be at least 2.")
    }
  } else {
    if (dim(obs)[time_dim] < 2) {
      stop("The length of time_dim must be at least 2.")
    }
  }
  ## lat and lon
  if (!is.null(exp)) {
    if (!is.numeric(lat) | length(lat) != dim(exp)[space_dim[1]]) {
      stop(paste0("Parameter 'lat' must be a numeric vector with the same ",
                  "length as the latitude dimension of 'exp' and 'obs'."))
    }
    if (!is.numeric(lon) | length(lon) != dim(exp)[space_dim[2]]) {
      stop(paste0("Parameter 'lon' must be a numeric vector with the same ",
                  "length as the longitude dimension of 'exp' and 'obs'."))
    }
  } else {
    if (!is.numeric(lat) | length(lat) != dim(obs)[space_dim[1]]) {
      stop(paste0("Parameter 'lat' must be a numeric vector with the same ",
                  "length as the latitude dimension of 'exp' and 'obs'."))
    }
    if (!is.numeric(lon) | length(lon) != dim(obs)[space_dim[2]]) {
      stop(paste0("Parameter 'lon' must be a numeric vector with the same ",
                  "length as the longitude dimension of 'exp' and 'obs'."))
    }
  } 
  stop_needed <- FALSE
  if (tail(lat, 1) < 70 | tail(lat, 1) > 90 | 
      head(lat, 1) > 30 | head(lat, 1) < 10) {
    stop_needed <- TRUE
  }
  #NOTE: different from s2dverification
  # lon is not used in the calculation actually. EOF only uses lat to do the
  # weight. So we just need to ensure the data is in this region, regardless
  # the order. 
  if (any(lon < 0)) {  #[-180, 180] 
    if (!(min(lon) > -90 & min(lon) < -70 & max(lon) < 50 & max(lon) > 30)) {
      stop_needed <- TRUE
    }
  } else {  #[0, 360]
    if (any(lon >= 50 & lon <= 270)) {
      stop_needed <- TRUE
    } else {
      lon_E <- lon[which(lon < 50)]
      lon_W <- lon[-which(lon < 50)]
      if (max(lon_E) < 30 | min(lon_W) > 290) {
        stop_needed <- TRUE
      }
    }
  }
  if (stop_needed) {
    stop(paste0("The typical domain used to compute the NAO is 20N-80N, ",
                "80W-40E. 'lat' or 'lon' is out of range."))
  }
  ## obsproj
  if (!is.logical(obsproj)  | length(obsproj) > 1) {
    stop("Parameter 'obsproj' must be either TRUE or FALSE.")
  }
  if (obsproj) {
    if (is.null(obs)) {
      stop("Parameter 'obsproj' set to TRUE but no 'obs' provided.")
    }
    if (is.null(exp)) {
      .warning("parameter 'obsproj' set to TRUE but no 'exp' provided.")
    }
  }
  ## 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.")
    }
  }

  #-------- Average ftime -----------
  if (!is.null(exp)) {
    exp_sub <- ClimProjDiags::Subset(exp, ftime_dim, ftime_avg, drop = FALSE)
    exp <- MeanDims(exp_sub, ftime_dim, na.rm = TRUE)
    ## Cross-validated PCs. Fabian. This should be extended to
    ## nmod and nlt by simple loops. Virginie
  }

  if (!is.null(obs)) {
    obs_sub <- ClimProjDiags::Subset(obs, ftime_dim, ftime_avg, drop = FALSE)
    obs <- MeanDims(obs_sub, ftime_dim, na.rm = TRUE)
  }

  if (!is.null(exp) & !is.null(obs)) {
    res <- Apply(list(exp, obs),
                 target_dims = list(exp = c(memb_dim, time_dim, space_dim),
                                    obs = c(memb_dim, time_dim, space_dim)),
                 fun = .NAO,
                 obsproj = obsproj, lat = lat, lon = lon,
                 ncores = ncores)
  } else if (!is.null(exp)) {
    res <- Apply(list(exp = exp),
                 target_dims = list(exp = c(memb_dim, time_dim, space_dim)),
                 fun = .NAO,
                 obsproj = obsproj, lat = lat, lon = lon, obs = NULL,
                 ncores = ncores)
  } else if (!is.null(obs)) {
    res <- Apply(list(obs = obs),
                 target_dims = list(obs = c(memb_dim, time_dim, space_dim)),
                 fun = .NAO,
                 obsproj = obsproj, lat = lat, lon = lon, exp = NULL,
                 ncores = ncores)
  }
  return(res)
}

.NAO <- function(exp = NULL, obs = NULL, lat, lon,
                 obsproj = TRUE, ncores = NULL) {
  # exp: [memb_exp, sdate, lat, lon]
  # obs: [memb_obs, sdate, lat, lon]
  if (!is.null(exp)) {
    ntime <- dim(exp)[2]
    nlat <- dim(exp)[3]
    nlon <- dim(exp)[4]
    nmemb_exp <- dim(exp)[1]
    nmemb_obs <- dim(obs)[1]
  } else {
    ntime <- dim(obs)[2]
    nlat <- dim(obs)[3]
    nlon <- dim(obs)[4]
    nmemb_obs <- dim(obs)[1]
  }

  if (!is.null(obs)) NAOO.ver <- array(NA, dim = c(ntime, nmemb_obs))
  if (!is.null(exp)) NAOF.ver <- array(NA, dim = c(ntime, nmemb_exp))

  for (tt in 1:ntime) {  #sdate

    if (!is.null(obs)) {
      ## Observed EOF excluding one forecast start year.
      obs_sub <- ClimProjDiags::Subset(obs, 2, c(1:ntime)[-tt], drop = FALSE)
      obs_EOF <- EOF(obs_sub, lat = lat, lon = lon, time_dim = names(ntime),
                     space_dim = c(names(nlat), names(nlon)), neofs = 1)

      ## Correct polarity of pattern.
      #NOTE: different from s2dverification
      # dim(obs_EOF$EOFs): [mode, lat, lon, member]
      for (imemb in 1:nmemb_obs) {
        if (0 < mean(obs_EOF$EOFs[1, which.min(abs(lat - 65)), , ], na.rm = T)) {
          obs_EOF$EOFs[1, , , imemb] <- obs_EOF$EOFs[1, , , imemb] * (-1)
        }
      }
#      obs_EOF$PCs <- obs_EOF$PCs * sign  # not used

      ## Project observed anomalies.
      PF <- ProjectField(obs, eof = obs_EOF, time_dim = names(ntime),
                         space_dim = c(names(nlat), names(nlon)), mode = 1)
      NAOO.ver[tt, ] <- PF[tt, ]
      ## Keep PCs of excluded forecast start year. Fabian.
    }

    if (!is.null(exp)) {
      if (!obsproj) {
        exp_sub <- ClimProjDiags::Subset(exp, 2, c(1:ntime)[-tt], drop = FALSE)
        #NOTE: different from s2dverification. Here, 'member' is considered.
        exp_EOF <- EOF(exp_sub, lat = lat, lon = lon, time_dim = names(ntime),
                       space_dim = c(names(nlat), names(nlon)), neofs = 1)

        ## Correct polarity of pattern.
        #NOTE: different from s2dverification
        for (imemb in 1:nmemb_exp) {
          if (0 < mean(exp_EOF$EOFs[1, which.min(abs(lat - 65)), , imemb], na.rm = T)) {
          exp_EOF$EOFs[1, , , imemb] <- exp_EOF$EOFs[1, , , imemb] * (-1)
          }
        }
#        exp_EOF$PCs <- exp_EOF$PCs * sign  # not used

        ### Lines below could be simplified further by computing
        ### ProjectField() only on the year of interest... (though this is
        ### not vital). Lauriane
        PF <- ProjectField(exp, eof = exp_EOF, time_dim = names(ntime),
                           space_dim = c(names(nlat), names(nlon)), mode = 1)
        NAOF.ver[tt, ] <- PF[tt, ]
        
      } else {
      ## Project forecast anomalies on obs EOF
      #NOTE: Because obs and exp have different nmemb, do ensemble mean to 
      #      obs_EOF$EOFs first, then expand the memb dim to be the same as exp.
      obs_EOF$EOFs <- apply(obs_EOF$EOFs, c(1, 2, 3), mean, na.rm = T)
      obs_EOF$EOFs <- array(obs_EOF$EOFs, dim = c(dim(obs_EOF$EOFs), as.numeric(nmemb_exp)))
      names(dim(obs_EOF$EOFs))[4] <- names(nmemb_obs)
      PF <- ProjectField(exp, obs_EOF, mode = 1)
      NAOF.ver[tt, ] <- PF[tt, ]
      }
    }
  }
  #NOTE: EOFs_obs is not returned because it's only the result of the last sdate
  #     (It is returned in s2dverification.)
  if (!is.null(exp) & !is.null(obs)) {
    return(list(exp = NAOF.ver, obs = NAOO.ver)) #, EOFs_obs = obs_EOF))
  } else if (!is.null(exp)) {
    return(list(exp = NAOF.ver))
  } else if (!is.null(obs)) {
    return(list(obs = NAOO.ver))
  }
}
