#' Helper functions to use various graphics devices
#'
#' These helper functions generalize the use of strings (e.g.,
#' `"svg"`, `"pdf"`) or graphic device functions (e.g.,
#' `grDevices::svg`, `grDevices::pdf`) for in-table plots.
#'
#' @param filename Passed through to the graphics device.
#' @param width,height Plot dimensions in pixels.
#' @param res The resolution of the plot; default is 300.
#' @param ... extra parameters passing to the graphics-device function.
#' @param dev Character (e.g., "svg", "pdf") or function (e.g.,
#' `grDevices::svg`, `grDevices::pdf`).
#' @name graphics_helpers
NULL

#' @describeIn graphics_helpers Generalize 'res' and 'filename across dev functions
#' @details
#' - `graphics_dev` generalizes the use of 'res' and plot dimensions
#'   across graphic devices. Raster-based devices (e.g., 'png',
#'   'jpeg', 'tiff', 'bmp') tend to use 'res' and the width/height
#'   units default to pixels. All other devices (e.g., 'pdf', 'svg')
#'   tend to use inches as the default units for width/height, and
#'   error when 'res' is provided.
#'
#'   The current heuristic is the look for the 'res' argument in the
#'   function's formals; if that is present, then it is assumed that
#'   the default units are in pixels, so 'width', 'height', and 'res'
#'   are passed through unmodified. If 'res' is not present, then
#'   'width' and 'height' are converted from pixels to inches, and
#'   'res' is not passed to the function
#'
#'   Another purpose of this function is to generalize the different
#'   graphic functions' use of 'file=' versus 'filename='.
#' @return 'graphics_dev': nothing, a plot device is opened
graphics_dev <- function(filename, width, height, res, ..., dev) {
  dev <- match.fun(dev)
  frmls <- names(formals(dev))
  dots <- list(...)
  if ("res" %in% frmls) {
    dots <- c(dots, list(width = width, height = height, res = res))
  } else {
    dots <- c(dots, list(width = width / res, height = height / res))
  }
  filenames <- c("file", "filename")
  found <- na.omit(match(frmls, filenames))[1]
  if (length(found)) {
    dots <- c(dots, setNames(filename, filenames[ found ]))
  } else {
    stop("could not find a 'file' argument in graphics dev")
  }
  do.call(dev, dots)
}

#' @describeIn graphics_helpers Determine if plot device is svg-like
#' @details
#' - `is_svg` determines if the plot device is svg-like, typically one
#'   of `"svg", `grDevices::svg`, or `svglite::svglite`
#' @return 'is_svg': logical
is_svg <- function(dev) {
  if (is.character(dev)) {
    return(grepl("svg", dev))
  }
  if (is.function(dev)) {
    return(any(sapply(formals(dev), function(f) {
      tryCatch(any(grepl("svg", as.character(f))),
               error = function(e) FALSE)
    })))
  }
  stop("unrecognized graphics 'dev': ", paste(class(dev), collapse = ","))
}

#' @describeIn graphics_helpers Determine filename extension
#' @details
#'
#' - `dev_chr` determines the filename extension for the applicable
#'   plot function; when `dev` is a string, then it is returned
#'   unchanged; when `dev` is a function, the formals of the function
#'   are checked for clues (i.e., default value of a `file=` argument)
#' @return `dev_chr`: character
#' @importFrom tools file_ext
dev_chr <- function(dev) {
  ext <- ""
  if (is.character(dev)) {
    ext <- if (dev == "svglite") "svg" else dev
  } else if (is.function(dev)) {
    frmls <- formals(dev)
    filearg <- grep("^file(name)?$", names(frmls), value = TRUE)
    if (length(filearg)) {
      ext <- grep("\\.[[:alpha:]]+$", unlist(sapply(frmls[filearg], as.character)),
                  value = TRUE)
      ext <- unique(tools::file_ext(ext))[1]
    }
  }
  if (is.na(ext) || !nzchar(ext)) {
    warning("could not determine filename extension from graphic device")
    ext <- ""
  }
  return(ext)
}

#' Combine file (or svg text) and parameters into a 'kableExtraInlinePlots' object
#'
#' @param filename Passed through to the graphics device.
#' @param file_ext Character, something like "png".
#' @param dev Character (e.g., "svg", "pdf") or function (e.g.,
#' @param width,height Plot dimensions in pixels.
#' @param res The resolution of the plot; default is 300.
#' @param del If the file is svg-like, then the default action is to
#'   read the file into an embedded SVG object; once done, the file is
#'   no longer used. The default action is to delete this file early,
#'   set this to 'FALSE' to keep the file.
#' @return list object, with class 'kableExtraInlinePlots'
make_inline_plot <- function(filename, file_ext, dev,
                             width, height, res,
                             del = TRUE) {
  if ((is_svg(file_ext) || is_svg(dev))) {
    svg_xml <- xml2::read_xml(filename)
    svg_text <- as.character(svg_xml)
    if (del) {
      unlink(filename)
      filename <- character(0)
    }
  } else {
    if (!is_latex()) {
      filename <- paste0("file:///", normalizePath(filename, winslash = "/"))
    }
    svg_text <- NULL
  }
  out <- list(path = filename, dev = file_ext, type = "line",
              width = width, height = height, res = res,
              svg_text = svg_text)
  class(out) <- c("kableExtraInlinePlots", "list")
  return(out)
}

#' Convert arguments for a single call into Map-able args
#'
#' @param ... Arbitrary arguments to be possibly converted into lists
#'   of arguments.
#' @param lengths Allowable lengths of the arguments, typically 1 and
#'   the length of the main variable (e.g., "x"). If 'NA' (default),
#'   it is not enforced.
#' @param passthru Character vector of variables to pass through with
#'   no conversion to lists of values. Extra names (not provided in
#'   `...`) are ignored.
#' @param notlen1vec Character vector of variables that are known to
#'   be length over 1 for a single plot call, so it will always be
#'   list-ified and extra care to ensure it is grouped correctly.
#'   Extra names (not provided in `...`) are ignored.
#' @param notlen1lst Character vector of variables that are lists, so
#'   the inner list length is not checked/enforced. (For example, if a
#'   single plot call takes an argument `list(a=1,b=2,d=3)` and the
#'   multi-data call creates three plots, then a naive match might
#'   think that the first plot would get `list(a=1)`, second plot gets
#'   `list(b=2)`, etc. Adding that list-argument to this 'notlen1lst'
#'   will ensure that the full list is passed correctly.) Extra names
#'   (not provided in `...`) are ignored.
#' @param ignore Character vector of variables to ignore, never
#'   returned. (Generally one can control this by not adding the
#'   variable in the first place, but having this here allows some
#'   sanity checks and/or programmatic usage.)
#' @return list, generally a list of embedded lists
listify_args <- function(..., lengths = NA,
                         passthru = c("x", "y"),
                         notlen1vec = c("lim", "xlim", "ylim"),
                         notlen1lst = c("minmax", "min", "max"),
                         ignore = c("same_lim")) {
  indots <- list(...)
  dotnms <- sapply(match.call(expand.dots=FALSE)$..., deparse)
  neednames <- if (is.null(names(indots))) {
    rep(TRUE, length(indots))
  } else !nzchar(names(indots))
  if (any(neednames)) {
    names(indots)[ neednames ] <- dotnms[ neednames ]
  }
  dots <- indots[ intersect(names(indots), passthru) ]

  # these are elements that are not typically length-1, so we need to
  # listify slightly differently
  nms <- intersect(names(indots), notlen1vec)
  if (length(nms)) {
    dots <- c(dots, Map(
      function(L, nm) {
        if (is.null(L)) return(list(NULL))
        if (!is.list(L)) return(list(L))
        if ((length(lengths) == 1 && is.na(lengths)) || length(L) %in% lengths) return(L)
        stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
      }, indots[ nms ], nms))
  }

  # these are a little special in that the argument must be a list
  # (regardless of its internal length)
  nms <- intersect(names(indots), notlen1lst)
  if (length(nms)) {
    dots <- c(dots, Map(
      function(L, nm) {
        if (is.null(L)) return(list(NULL))
        if (!length(L)) return(list(list()))
        if (!is.list(L[[1]])) return (list(L))
        if ((length(lengths) == 1 && is.na(lengths)) || length(L) %in% lengths) return(L)
        stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
      }, indots[ nms ], nms))
  }

  # the remainder, those that we don't know about explicitly and are
  # not intentionally ignored
  nms <- setdiff(names(indots), c(passthru, notlen1vec, notlen1lst, ignore))
  if (length(nms)) {
    dots <- c(dots, Map(
      function(V, nm) {
        if (is.null(V)) return(list(NULL))
        if (is.function(V)) return(list(V))
        if ((length(lengths) == 1 && is.na(lengths)) || length(V) %in% lengths) return(V)
        stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
      }, indots[ nms ], nms))
  }

  dots
}
