#===== Implementation utility =====


#' get_class_from_xpath
#' @description Gets r2ogs6 class name from an xpath-like expression
#' @param xpath string: An xpath expression. Works for path-like xpaths only
#' @return string: The class name.
#' @noRd
get_class_from_xpath <- function(xpath){

  assertthat::assert_that(assertthat::is.string(xpath))

  xpaths_for_classes <- xpaths_for_classes

  for(i in seq_len(length(xpaths_for_classes))){
    if((assertthat::is.string(xpaths_for_classes[[i]]) &&
        xpaths_for_classes[[i]] == xpath) ||
       (length(xpaths_for_classes[[i]]) > 1 &&
        xpath %in% xpaths_for_classes[[i]])){
      return(names(xpaths_for_classes)[[i]])
    }
  }

  return(NULL)
}


#' get_tag_from_class
#' @description Utility function, returns the tag name of a r2ogs6 class
#' @param class_name string: The name of a r2ogs6 class
#' @return string: The tag name corresponding to \code{class_name}
#' @noRd
get_tag_from_class <- function(class_name) {

  assertthat::assert_that(assertthat::is.string(class_name))

  xpaths_for_classes <- xpaths_for_classes

  xpath <- xpaths_for_classes[[class_name]]
  split_xpath <- unlist(strsplit(xpath[[1]], "/", fixed = TRUE))
  tag_name <- split_xpath[[length(split_xpath)]]

  return(tag_name)
}


#' get_tag_from_xpath
#' @description Gets the XML tag name from an xpath expression
#' @param xpath string: An xpath expression. Works for path-like xpaths only
#' @return string: The XML tag name
#' @noRd
get_tag_from_xpath <- function(xpath){

  xpath_split <- unlist(strsplit(xpath, "/", fixed = TRUE))
  tag_name <- xpath_split[[length(xpath_split)]]

  return(tag_name)
}


#' get_prj_top_level_tags
#' @description Gets top level .prj tags along with info if they are required.
#' @return list: List of lists.
#' @noRd
get_prj_top_level_tags <- function(){

  prj_reduxml <- system.file("extdata/xml_redux/", "prj_redu.xml",
                             package = "r2ogs6")

  xml_doc <- xml2::read_xml(prj_reduxml)

  prj_tag_info <- lapply(xml2::xml_children(xml_doc), function(x){
      list(tag_name = xml2::xml_name(x),
           is_required = as.logical(xml2::xml_attrs(x)[["required"]]))
  })

  return(prj_tag_info)
}


#===== Coercion utility =====


#' coerce_string_to_numeric
#' @description If an object is of type string, coerces it to a numeric type
#' @param obj object: Any object
#' @return numeric if \code{obj} was a string, else unchanged \code{obj}
#' @noRd
coerce_string_to_numeric <- function(obj){

  if(assertthat::is.string(obj)){
    obj <- trimws(gsub("\r?\n|\r|\\s+", " ", obj))
    obj <- as.double(unlist(strsplit(obj, " ")))
  }

  return(obj)
}


#' coerce_names
#' @description Validator function for a parameter vector
#' @param vector vector: Vector of parameters
#' @param names character: How the vector elements will be named as
#' per default
#' @return vector: Named vector where the names correspond to \code{names}
#' @noRd
coerce_names <- function(vector, names) {

  assertthat::assert_that(is.vector(vector))
  assertthat::assert_that(is.character(names))
  assertthat::assert_that(length(vector) == length(names))

  sorted_param_names <- sort(names(vector))
  sorted_names <- sort(names)

  if(is.null(names(vector)) ||
     (!is.null(names(vector)) &&
      any(sorted_param_names != sorted_names))){

    names(vector) <- names

    message(paste0(
      "Renaming elements of ",
      deparse(quote(vector)),
      " to fit their default names: '",
      paste(names, collapse = "', '"),
      "'"))
  }

  return(invisible(vector))
}


#' is_null_or_coerce_names
#' @description Validator function for a parameter list or vector or \code{NULL}
#' @param obj A list (or vector) of parameters
#' @param names How the list elements will be named as per default
#' @noRd
is_null_or_coerce_names <- function(obj, names){

  if(!is.null(obj)){
    obj <- coerce_names(obj, names)
  }

  return(invisible(obj))
}


#' clean_imported_list
#' @description Cleans an imported list because sometimes strings containing
#' only newline characters and spaces get imported in
#' @param list list: A list
#' @noRd
clean_imported_list <- function(list){

  assertthat::assert_that(is.list(list))

  cleaned_list <- list()

  for(i in seq_len(length(list))){
    if(assertthat::is.string(list[[i]]) &&
       stringr::str_remove_all(list[[i]], "[\n|[:space:]]") == ""){
      next
    }
    cleaned_list <- c(cleaned_list, list(list[[i]]))
    names(cleaned_list)[[length(cleaned_list)]] <- names(list)[[i]]
  }

  return(invisible(cleaned_list))
}


#' as_dir_path
#' @description Checks if a given path ends on \code{/}
#' @param path string: A path
#' @noRd
as_dir_path <- function(path){

  assertthat::assert_that(assertthat::is.string(path))

  path <- gsub("\\", "/", path, fixed = TRUE)

  nchar <- nchar(path)

  if(substring(path, nchar, nchar) != "/"){
    path <- paste0(path, "/")
  }

  return(invisible(path))
}

#' as_dir_path2
#' @description Removes leading \code{./} from a given path
#' @param file_path string: A path
#' @noRd
as_dir_path2 <- function(file_path){

  assertthat::assert_that(assertthat::is.string(file_path))

  if(substr(file_path,1,2) %>% stringr::str_detect("./")){
    file_path <- sub("\\./", "", file_path)
  }
  return(invisible(file_path))
}

#' filter_invalid_xml
#' @description Filters invalid XML paths out of a vector
#' @param paths character: Vector of (maybe-)XML paths
#' @param encoding string: Optional: XML encoding. Defaults to ISO-8859-1
#' @param print_messages flag: Optional: Print error messages? Defaults to
#' \code{TRUE}
#' @return character: Vector of invalid XML paths
#' @noRd
filter_invalid_xml <- function(paths,
                               encoding = "ISO-8859-1",
                               print_messages = TRUE){

  invalid_paths <- character()

  for(i in seq_len(length(paths))){
    out <- tryCatch(
      {
        xml2::read_xml(paths[[i]],
                       encoding = encoding)
      },
      error = function(cond){
        if(print_messages){
          message(paste("\nxml2::read_xml() failed for",
                        paths[[i]], ". Original error message:"))
          message(cond)
        }
        invalid_paths <<- c(invalid_paths, paths[[i]])
      }
    )
  }

  return(invalid_paths)
}


#===== Validation utility =====


#' are_numbers
#' @description Checks if objects are numbers
#' @param ... Ellipsis
#' @noRd
are_numbers <- function(...){

  lapply(list(...), function(x){
    assertthat::assert_that(assertthat::is.number(x))
  })

  return(invisible(TRUE))
}


#' are_null_or_numbers
#' @description Checks if objects are either \code{NULL} or numbers
#' @param ... Ellipsis
#' @noRd
are_null_or_numbers <- function(...){

  lapply(list(...), function(x){
    if(!is.null(x)){
      assertthat::assert_that(assertthat::is.number(x))
    }
  })

  return(invisible(TRUE))
}


#' are_numeric
#' @description Checks if objects are numeric
#' @param ... Ellipsis
#' @noRd
are_numeric <- function(...){

  lapply(list(...), function(x){
    assertthat::assert_that(is.numeric(x))
  })

  return(invisible(TRUE))
}


#' are_null_or_numeric
#' @description Checks if objects are either \code{NULL} or numeric
#' @param ... Ellipsis
#' @noRd
are_null_or_numeric <- function(...){

  lapply(list(...), function(x){
    if(!is.null(x)){
      assertthat::assert_that(is.numeric(x))
    }
  })

  return(invisible(TRUE))
}


#' are_strings
#' @description Checks if objects are strings
#' @param ... Ellipsis
#' @noRd
are_strings <- function(...){

  lapply(list(...), function(x){
    assertthat::assert_that(assertthat::is.string(x))
  })

  return(invisible(TRUE))
}


#' are_null_or_strings
#' @description Checks if objects are either \code{NULL} or strings
#' @param ... Ellipsis
#' @noRd
are_null_or_strings <- function(...){

  lapply(list(...), function(x){
    if(!is.null(x)){
      assertthat::assert_that(assertthat::is.string(x))
    }
  })

  return(invisible(TRUE))
}


#' are_string_flags
#' @description Checks if objects are strings reading either "true" or "false"
#' @param ... Ellipsis
#' @noRd
are_string_flags <- function(...){

  lapply(list(...), function(x){
    assertthat::assert_that(assertthat::is.string(x))
    assertthat::assert_that(x %in% c("true", "false"))
  })

  return(invisible(TRUE))
}


#' are_null_or_string_flags
#' @description Checks if objects are either \code{NULL} or strings reading
#' either "true" or "false"
#' @param ... Ellipsis
#' @noRd
are_null_or_string_flags <- function(...){

  lapply(list(...), function(x){
    if(!is.null(x)){
      are_string_flags(x)
    }
  })

  return(invisible(TRUE))
}


#' is_wrapper_list
#' @description Checks if a list consists only of elements of class
#' \code{element_class}
#' @param list list: List to check
#' @param element_class string: Class each element of \code{list} should have
#' @noRd
is_wrapper_list <- function(list, element_class) {

  assertthat::assert_that(is.list(list))

  lapply(list, function(x){
    assertthat::assert_that(any(grepl(element_class, class(x), fixed = TRUE)))
  })

  return(invisible(TRUE))
}


#' is_null_or_wrapper_list
#' @description Checks if an object is either \code{NULL} or a list of elements
#' of class \code{element_class}
#' @param obj list | NULL: Object to check
#' @param element_class string: Class each element of \code{obj} should have
#' @noRd
is_null_or_wrapper_list <- function(obj, element_class) {

  if(!is.null(obj)){
    assertthat::assert_that(is.list(obj))

    lapply(obj, function(x){
      assertthat::assert_that(any(grepl(element_class, class(x), fixed = TRUE)))
    })
  }

  return(invisible(TRUE))
}


#' is_null_or_has_class
#' @description Checks if an object is either \code{NULL} or a class object of
#' class \code{class_name}
#' @param obj The object to check
#' @param class_name The name of the expected class
#' @noRd
is_null_or_has_class <- function(obj, class_name){

  if(!is.null(obj)){
      assertthat::assert_that(class(obj) == class_name)
  }

  return(invisible(TRUE))
}


#--- File utility ------------------------------------------------------------

#' make_abs_path
#' @description Creates an absolute file path based on a given file name or path
#'  and a reference path. The reference path will be adjusted if the file path
#'  is relative (e.g.  \code{../foo/bar.baz}). A file with the created absolute
#'  path should exist.
#' @param file_path string: Name or path to file.
#' @param ref_path string: Reference path whre file_path will be combined with.
#' Will be made absolute if relative path is given.
#' @noRd
make_abs_path <- function(file_path, ref_path, force=F){

  assertthat::assert_that(assertthat::is.string(file_path))
  assertthat::assert_that(assertthat::is.string(ref_path))

  # make shure that ref_path is absolute and ends with "/"
  ref_path <- normalizePath(ref_path, mustWork = T)
  ref_path <- as_dir_path(ref_path)
  file_path <- as_dir_path2(file_path)

  # case1: if file_path is absolute
  if((substr(file_path,1,1)=="/")| # abspath on unix
     (substr(file_path,1,3) %>% stringr::str_detect("[:alpha:]:\\\\"))){ # windows?

    message(paste("file_path", file_path, "is already asolute."))
    if(isTRUE(force)){
      file_path <- paste0(ref_path, basename(file_path))
      message(paste0("Forced to convert to: ", ref_path, file_path))
    }
  }
  # case2: if file_path consist of filename only
  else if(basename(file_path)==file_path){
    file_path <- paste0(ref_path,file_path)
  }
  # case3: if file_path is in parent dir(s) (contains "../")
  else if(stringr::str_detect(file_path, "\\.\\./")){
    cds <- stringr::str_count(file_path, "\\.\\./")
    file_path <- gsub("\\.\\./", "", file_path)
    # cd.. through ref_path
    for(i in seq(cds)){
      ref_path <- sub("\\w+/$", "", ref_path)
    }
    file_path <- paste0(ref_path,file_path)
  }
  # case4: file is in child dir
  else {

    file_path_norm <- normalizePath(dirname(file_path), mustWork = T)

    assertthat::assert_that(
      stringr::str_detect(file_path_norm, ref_path),
      msg = paste(file_path, "is neither an absolute path nor a reference to",
                  "a file in", ref_path, "or in a child or parent directory."))

    file_path <- paste0(ref_path,file_path)
  }

  assertthat::assert_that(file.exists(file_path))

  return(file_path)
}