.SetDimNames <- function(dimnames = NULL, restore = FALSE) {
  if (!restore) { 
    # Check dimnames
    if (!is.character(dimnames)) {
      stop("Parameter 'dimnames' must be a character string vector.")
    }
    if (length(dimnames) > 20) {
      stop("Parameter 'dimnames' can contain only up to 20 dimension names.")
    }
    if (sum(sapply(dimnames, nchar) > 20) > 0) {
      stop("Dimension names provided in 'dimnames' can only be up to 20 characters long.")
    }
    
    save(dimnames, file = paste0(tempdir(), '/s2dv.dimnames.RData'))
  } else {
    if (file.exists(paste0(tempdir(), '/s2dv.dimnames.RData'))) {
      file.remove(paste0(tempdir(), '/s2dv.dimnames.RData'))
    }
  }
}

.GetDimNames <- function() {
  if (file.exists(paste0(tempdir(), '/s2dv.dimnames.RData'))) {
    load(paste0(tempdir(), '/s2dv.dimnames.RData'))
    dimnames
  } else {
    c('dataset', 'var', 'level', 'member', 'sdate', 'ftime', 
      'lat', 'lon', 'test')
  }
}

PlotTimeSeries <- function(var, curves_along = NULL, colour_along = NULL, 
                           mean_along = NULL, interval_along = NULL, 
                           error_bar_along = NULL, lower = 1, test = 2, upper = 3,
                           interval_type = "errorbar", x_axis_dim = NULL, first_date = NULL, 
                           sdates = NULL, period = NULL, sdate_period = NULL,
                           title = NULL, xtitle = NULL, ytitle = NULL,
                           ref_dimnames = .GetDimNames(),
                           file_name = NULL, file_path = NULL,
                           file_width = NA, file_height = NA, 
                           file_size_units = "in", add = FALSE) {
  library(ggplot2)

  # Check 'var'. Forcing 'var' to contain only a numeric array with the
  # data, whichever the input type. This way a common code can be used
  # afterwards, independently of the input type.
  # Also taking the dimension names, if available.
  dimnames <- NULL
  metadata <- NULL
  if (class(var) == 'multiCDM') {
    # Change 'var' to contain only the data array. The rest of metadata
    # is moved to 'metadata'.
    attr_bk <- attributes(var)
    metadata <- var[-(which(names(var) == 'Data'))]
    attr_bk[['names']] <- attr_bk[['names']][-which(attr_bk[['names']] == 'Data')]
    attributes(metadata) <- attr_bk
    var <- var$Data
  }
  if (is.numeric(var) && is.array(var) && length(var) > 0) {
    if (all(is.na(var))) {
      stop("All values in 'var' are NA.")
    } 
    dimnames <- attr(var, 'dimensions')
    if (is.null(dimnames)) {
      dimnames <- names(dim(var))
    }
  } else {
    stop("Parameter 'var' must be a 'multiCDM' object or a numeric array.")
  }
  # Check there are no repeated dimension names, for safety.
  if (!is.null(dimnames)) {
    if (length(dimnames) != length(unique(dimnames))) {
      stop("There must be no repeated names in the provided dimension names in 'var'.")
    }
  }

  # Convert array into data frame for ggplot2.
  dims <- dim(var)
  var <- as.data.frame.table(var)
  if (is.null(dimnames)) {
    dimnames <- paste0('dim', 1:length(dims))
  }
  names(var) <- c(dimnames, 'value')

  # Check ref_dimnames
  if (!is.character(ref_dimnames)) {
    stop("Parameter 'ref_dimnames' must be a vector of character strings.")
  }
  if (length(ref_dimnames) != length(.GetDimNames())) {
    stop("Parameter 'ref_dimnames' must contain as many dimension names as defined in s2dv::.GetDimNames().")
  }

  # Check that the '_along' parameters contain valid dimension names, 
  # and convert always to integer numbers. NA values are also allowed to
  # deactivate the corresponding option. So, afther the parameter 
  # checks all the '_along' parameters will be either a valid dimension
  # index or a NA value.

  # The following function checks a single '_along' parameter, processes
  # it and returns its new value. 
  # 'param' is the value provided for one of the '_along' parameters. 
  # 'dimnames' is the character string vector of detected dimension 
  # names in the input 'var' (can also be NULL). 'seek_names' is a 
  # vector of dimensions to look for and to assign to the '_along' 
  # parameter if no value has been provided for it. The index of the 
  # first found dimension name in 'seek_names' is assigned to the 
  # parameter. If no dimension is provided or found, NA (deactivation) 
  # is assigned to the '_along' parameter'.
  CheckDimensionSpec <- function(param, dimnames, seek_names = NULL) {
    if (is.null(param)) {
      if (is.null(dimnames)) {
        param <- NA
      } else {
        param <- NA
        found <- FALSE
        i <- 1
        while ((i <= length(seek_names)) && !found) {
          if (seek_names[i] %in% dimnames) {
            # Do not consider dimensions of length 1 while seeking
            if (dims[grep(seek_names[i], dimnames)] > 1) {
              param <- seek_names[i]
              found <- TRUE
            }
          }
          i <- i + 1
        }
      }
    }
    # If it's a NA value, transform to a logical NA always, for safety.
    if (is.na(param)) {
      param <- as.logical(param)
    }
    if (is.character(param)) {
      if (param %in% dimnames) {
        param <- which(dimnames == param)
      } else {
        stop("All the dimension names provided as character strings in the '_along' parameters must be one of the dimension names of 'var'.")
      }
    }
    if (is.numeric(param)) {
      param <- round(param)
      if (param > length(dimnames)) {
        stop("All the dimension indices provided as integer numbers in the '_along' parameters must be in the range 1:length(dim(var)).")
      }
    } else if (!is.na(param)) {
      stop("Each provided '_along' parameter must be either a character string, a numeric value or NA.")
    }

    param
  }

  # Check curves_along. Time dimensions are seeked in the input
  # dimension names.
  curves_along <- CheckDimensionSpec(curves_along, dimnames, 
                                     ref_dimnames[6:5])

  # Check colour_along.
  if (all(ref_dimnames[5:6] %in% dimnames)) {
    colour_along_priorities <- ref_dimnames[5]
  } else {
    colour_along_priorities <- ref_dimnames[1:4]
  }
  colour_along <- CheckDimensionSpec(colour_along, dimnames,
                                     colour_along_priorities)

  # Check mean_along
  if (is.null(mean_along) && all(ref_dimnames[5:6] %in% dimnames)) {
    mean_along <- ref_dimnames[5]
  }
  mean_along <- CheckDimensionSpec(mean_along, dimnames)

  # Check interval_along
  if (is.null(interval_along) && all(ref_dimnames[c(6, 9)] %in% dimnames)) {
    interval_along <- ref_dimnames[9]
  }
  interval_along <- CheckDimensionSpec(interval_along, dimnames)

  # Check error_bar_along
  if (is.null(error_bar_along) && all(ref_dimnames[c(5, 9)] %in% dimnames) &&
      !(ref_dimnames[6] %in% dimnames)) {
    error_bar_along <- ref_dimnames[9]
  }
  error_bar_along <- CheckDimensionSpec(error_bar_along, dimnames)

  is.POSIXct <- function(date) 'POSIXct' %in% class(date)
  # Check first_date
  if (is.null(first_date)) {
    first_date <- as.POSIXct('00000101', format = '%Y%m%d')
  }
  if (!is.POSIXct(first_date)) {
    stop("Parameter 'first_date' must be a POSIXct object.")
  }

  # Check sdates
  if (!is.null(sdates)) {
    if (!all(sapply(sdates, is.POSIXct))) {
      stop("Parameter 'sdates' must be a vector of POSIXct objects.")
    }
  }

  # Check period
  if (is.null(period)) {
    period <- 1
  }
  if (is.numeric(period)) {
    period <- paste(period, 'month')
  } else if (!is.character(period)) {
    stop("Parameter 'period' must be numeric or a character string.")
  }

  # Check sdate_period
  if (is.null(sdate_period)) {
    ftime_pos <- grep(ref_dimnames[6], dimnames, fixed = TRUE)
    if (length(ftime_pos) > 0) {
      sdate_period <- paste(dims[ftime_pos] * as.numeric(strsplit(period, ' ')[[1]][1]),
                            strsplit(period, ' ')[[1]][2])
    } else {
      sdate_period <- 1
    }
  }
  if (is.numeric(sdate_period)) {
    sdate_period <- paste(sdate_period, 'year')
  } else if (!is.character(sdate_period)) {
    stop("Parameter 'sdate_period' must be numeric or a character string.")
  }

  # Check x_axis_dim.
  x_axis_dim <- CheckDimensionSpec(x_axis_dim, dimnames,
                                   ref_dimnames[6:5])
  # If no time dimension is found, pick the longest one for the x axis.
  if (is.na(x_axis_dim)) {
    x_axis_dim <- which.max(dims)
    print(paste0("No 'x_axis_dim' specified and no time dimension found in 'var'. Taking the longest dimension (", 
                 dimnames[x_axis_dim], ") to be represented along x axis."))
    # Put the x_axis_dim as last dim before asigning 'dates' (indices 
    # in this case).
    reorder <- c((1:length(dims))[-x_axis_dim], x_axis_dim)
    var <- var[do.call(order, lapply(reorder, function(x) `[[`(var, x))), ]
    var$date <- 1:dims[x_axis_dim]
  # If time dimension is found, reorder the data frame with the time 
  # dimension at the right-most position so that dates found in 
  # metadata match, as they come, with the reordered values.
  } else {
    ftime_pos <- grep(ref_dimnames[6], dimnames, fixed = TRUE)
    sdate_pos <- grep(ref_dimnames[5], dimnames, fixed = TRUE)
    reorder <- c(1:length(dims))
    n_ftime <- 1
    if (length(sdate_pos) > 0) {
      # Reorder with sdate as right-most dim
      reorder <- c(reorder[-sdate_pos], sdate_pos)
    }
    # If ftime is one of the dimensions
    if (length(ftime_pos) > 0) {
      # Reorder with ftime as right-most dim
      reorder <- c(reorder[-(ftime_pos - ifelse(length(sdate_pos) > 0, 1, 0))], ftime_pos)
      n_ftime <- dims[ftime_pos]
    # If sdate is one of the dimensions
    }
    var <- var[do.call(order, lapply(reorder, function(x) `[[`(var, x))), ]
    if ('Dates' %in% names(metadata)) {
      offset <- (metadata$Dates$end[1] - metadata$Dates$start[1]) / 2
      var$date <- metadata$Dates$start + offset
      sdates <- metadata$Dates$start[seq(1, length(metadata$Dates$start), n_ftime)]
    } else {
      if (is.null(sdates)) {
        if (length(sdate_pos) > 0) {
          sdates <- seq(first_date, length = dims[sdate_pos], by = sdate_period)
        } else {
          sdates <- first_date
        }
      }
      if (length(ftime_pos) > 0) {
        var$date <- do.call(c, lapply(sdates, seq, length = n_ftime, by = period))
      } else {
        var$date <- sdates
      }
    }
  }

  # Check title
  if (is.null(title)) {
    title <- NA
    if (!is.null(metadata)) {
      if (!is.null(attr(metadata$Variable, 'longname'))) {
        title <- attr(metadata$Variable, 'longname')
      }
    }
  } else if (is.na(title)) {
    title <- as.logical(title)
  } else if (!is.character(title)) {
    stop("Parameter 'title' must be a character string.")
  }

  # Check xtitle
  if (is.null(xtitle)) {
    xtitle <- NA
    xtitle <- dimnames[x_axis_dim]
  } else if (is.na(xtitle)) {
    xtitle <- as.logical(xtitle)
  } else if (!is.character(xtitle)) {
    stop("Parameter 'xtitle' must be a character string.")
  }

  # Check ytitle
  if (is.null(ytitle)) {
    ytitle <- 'value'
    if (!is.null(metadata)) {
      ytitle <- metadata$Variable$varName
    }
  } else if (is.na(ytitle)) {
    ytitle <- as.logical(ytitle)
  } else if (!is.character(ytitle)) {
    stop("Parameter 'ytitle' must be a character string.")
  }

  # Check device parameters
  if (!is.character(file_name) && !is.null(file_name)) {
    stop("Parameter 'file_name' must be a character string.")
  }
  if (is.null(file_name) && !is.null(file_path)) {
    print("'file_name' not specified for the 'file_path', it will be ignored.")
  } else if (!is.null(file_name)) {
    # Chek file_path
    if (is.null(file_path)) {
      file_path <- getwd()
    } else if (!is.character(file_path)) {
      print("'file_path' must be a character string of the desired path. Saving to default (working directory).")
      file_path <- getwd()
    } ## TODO: check if it is a correct pathname?
    
    # Check size parameters
    if (!is.numeric(file_width) && !is.na(file_width)) {
      print("'file_width' must be a number, setting width to default.")
      file_width <- NA
    }
    if (!is.numeric(file_height) && !is.na(file_height)) {
      print("'file_height' must be a number, setting height to default.")
      file_height <- NA
    }
    if (!(file_size_units %in% c("cm", "in", "mm"))) {
      print("'file_size_units' must be 'cm', 'in' or 'mm', setting units to default (inches).")
      file_size_units <- "in"
    }
  }
  # Plot
  group_dims <- dimnames
  if (!is.na(curves_along)) {
    group_dims <- group_dims[-curves_along]
  }
  if (!add) {
    g <- ggplot()

    if (!is.na(interval_along) || !is.na(error_bar_along)) {
      if ( !interval_type %in% c('errorbar', 'line')) {
        stop("Interval type must be either line or errobar.")
      }
      upper <- levels(var[, interval_along])[upper]
      test  <- levels(var[, interval_along])[test]
      lower <- levels(var[, interval_along])[lower]
      if (interval_type == "errorbar") {
        g <- g + geom_point(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == test]), colour = "red") + 
              geom_errorbar(aes(x = var$date[var[, interval_along] == test], ymin = var$value[var[, interval_along] == lower], 
                         ymax = var$value[var[, interval_along] == upper]),colour = "red")
      } else if (interval_type == "line") {
        g <- g + geom_line(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == test]), colour = "red") + 
                   geom_line(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == lower]), colour = "red", linetype = 2) +  
                     geom_line(aes(x = var$date[var[, interval_along] == test], 
                         y = var$value[var[, interval_along] == upper]), colour = "red", linetype = 2) 
      }
    } else {
      plot_aes <- aes(x = var$date, y = var$value, group = interaction(mget(group_dims)))
      if (!is.na(colour_along)) {
        plot_aes[['colour']] <- var[, colour_along]
      }
      if (!is.na(curves_along)) {
        g <- g + geom_line(data = var, plot_aes, size = 0.4)
      } else {
        g <- g + geom_point(data = var, plot_aes, size = 0.4)
      }
    }
    g <- g + labs(title = na.omit(title)) + xlab(xtitle) + ylab(ytitle)
    # Adjust scale for colour
    if (!is.na(colour_along)) {
      g <- g + labs(colour = dimnames[colour_along])
      # If the dimension coloured along is the members
      if (dimnames[colour_along] == ref_dimnames[4]) {
        if (!is.null(metadata$Datasets)) {
          labels <- names(metadata$Datasets[[1]]$InitializationDates)
        } else {
          labels <- paste(ref_dimnames[4], 1:dims[colour_along])
        }
      # If the dimension coloured along is the sdates
      } else if (dimnames[colour_along] == ref_dimnames[5]) {
        if ('Dates' %in% names(metadata)) {
          labels <- sdates
        } else {
          labels <- paste(ref_dimnames[5], 1:dims[colour_along])
        }
      }
      g <- g + scale_colour_discrete(labels = labels)
    }
    # Draw mean line
    if (!is.na(mean_along)) {
      g <- g + stat_smooth(mapping = aes_string(x = var$date, y = var$value, 
                           group = var[, mean_along], 
                           colour = var[, mean_along], 
                           fill = var[, mean_along]), 
                           alpha = 0.3, show.legend = FALSE)
    }
  # If adding the plot, no ggplot object is created
  } else {
    if (!is.na(interval_along) || !is.na(error_bar_along)) {
      if ( !interval_type %in% c('errorbar', 'line')) {
        stop("Interval type must be either line or errobar.")
      }
      upper <- levels(var[, interval_along])[upper]
      test  <- levels(var[, interval_along])[test]
      lower <- levels(var[, interval_along])[lower]
      if (interval_type == "errorbar") {
        g <-  geom_point(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == test]), colour = "red") + 
          geom_errorbar(aes(x = var$date[var[, interval_along] == test], ymin = var$value[var[, interval_along] == lower], ymax = var$value[var[, interval_along] == upper]), 
                        colour = "red")
      } else if (interval_type == "line") {
        g <-  geom_line(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == test]), colour = "red") + 
          geom_line(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == lower]), colour = "red", linetype = 2)
          + geom_line(aes(x = var$date[var[, interval_along] == test], y = var$value[var[, interval_along] == lower], y = var$value[var[, interval_along] == upper]), 
                                                                 colour = "red", linetype = 2)
      }
    } else {
      if (!is.na(curves_along)) {
        g <- geom_line(aes_string(data = var, x = var[,6], y = var$value, group = interaction(mget(group_dims)), colour = var[, colour_along]), size = 0.4)
      } else {
        g <- geom_point(aes_string(data = var, x = var[,6], y = var$value, group = interaction(mget(group_dims)), colour = var[, colour_along]), size = 0.4)
      }
    }
  }

  # Return ggplot2 object or save to file
  if (!is.null(file_name)) {
    # Save to file
    g <- ggsave(filename = file_name, plot = g, path = file_path,
                width = file_width, height = file_height, 
                units = file_size_units)
    invisible(NULL)
  } else {
    g
  }
} 
