startPNG <- function(filename, width, height, res, ...) {
  pngfun <- if ((getOption('shiny.useragg') %||% TRUE) && is_installed("ragg")) {
    ragg::agg_png
  } else if (capabilities("aqua")) {
    # i.e., png(type = 'quartz')
    grDevices::png
  } else if ((getOption('shiny.usecairo') %||% TRUE) && is_installed("Cairo")) {
    Cairo::CairoPNG
  } else {
    # i.e., png(type = 'cairo')
    grDevices::png
  }

  args <- list2(filename = filename, width = width, height = height, res = res, ...)

  # It's possible for width/height to be NULL/numeric(0) (e.g., when using
  # suspendWhenHidden=F w/ tabsetPanel(), see rstudio/shiny#1409), so when
  # this happens let the device determine what the default size should be.
  if (length(args$width) == 0) args$width <- NULL
  if (length(args$height) == 0) args$height <- NULL

  # Set a smarter default for the device's bg argument (based on thematic's global state).
  # Note that, technically, this is really only needed for CairoPNG, since the other
  # devices allow their bg arg to be overridden by par(bg=...), which thematic does prior
  # to plot-time, but it shouldn't hurt to inform other the device directly as well
  if (is.null(args$bg) && isNamespaceLoaded("thematic")) {
    args$bg <- getThematicOption("bg", "white")
    # auto vals aren't resolved until plot time, so if we see one, resolve it
    if (isTRUE("auto" == args$bg)) {
      args$bg <- getCurrentOutputInfo()[["bg"]]()
    }
  }

  # Handle both bg and background device arg
  # https://github.com/r-lib/ragg/issues/35
  fmls <- names(formals(pngfun))
  if (("background" %in% fmls) && (!"bg" %in% fmls)) {
    if (is.null(args$background)) {
      args$background <- args$bg
    }
    args$bg <- NULL
  }

  do.call(pngfun, args)
  # Call plot.new() so that even if no plotting operations are performed at
  # least we have a blank background. N.B. we need to set the margin to 0
  # temporarily before plot.new() because when the plot size is small (e.g.
  # 200x50), we will get an error "figure margin too large", which is triggered
  # by plot.new() with the default (large) margin. However, this does not
  # guarantee user's code in func() will not trigger the error -- they may have
  # to set par(mar = smaller_value) before they draw base graphics.
  op <- graphics::par(mar = rep(0, 4))
  tryCatch(
    graphics::plot.new(),
    finally = graphics::par(op)
  )

  grDevices::dev.cur()
}

#' Capture a plot as a PNG file.
#'
#' The PNG graphics device used is determined in the following order:
#'   * If the ragg package is installed (and the `shiny.useragg` is not
#'    set to `FALSE`), then use [ragg::agg_png()].
#'   * If a quartz device is available (i.e., `capabilities("aqua")` is
#'    `TRUE`), then use `png(type = "quartz")`.
#'   * If the Cairo package is installed (and the `shiny.usecairo` option
#'    is not set to `FALSE`), then use [Cairo::CairoPNG()].
#'   * Otherwise, use [grDevices::png()]. In this case, Linux and Windows
#'    may not antialias some point shapes, resulting in poor quality output.
#'
#' @details
#'   A `NULL` value provided to `width` or `height` is ignored (i.e., the
#'   default `width` or `height` of the graphics device is used).
#'
#' @param func A function that generates a plot.
#' @param filename The name of the output file. Defaults to a temp file with
#'   extension `.png`.
#' @param width Width in pixels.
#' @param height Height in pixels.
#' @param res Resolution in pixels per inch. This value is passed to the
#'   graphics device. Note that this affects the resolution of PNG rendering in
#'   R; it won't change the actual ppi of the browser.
#' @param ... Arguments to be passed through to the graphics device. These can
#'   be used to set the width, height, background color, etc.
#'
#' @return A path to the newly generated PNG file.
#'
#' @export
plotPNG <- function(func, filename=tempfile(fileext='.png'),
                    width=400, height=400, res=72, ...) {
  dv <- startPNG(filename, width, height, res, ...)
  on.exit(grDevices::dev.off(dv), add = TRUE)
  func()

  filename
}

createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
  force(which)

  promises::new_promise_domain(
    wrapOnFulfilled = function(onFulfilled) {
      force(onFulfilled)
      function(...) {
        old <- dev.cur()
        dev.set(which)
        on.exit(dev.set(old))

        onFulfilled(...)
      }
    },
    wrapOnRejected = function(onRejected) {
      force(onRejected)
      function(...) {
        old <- dev.cur()
        dev.set(which)
        on.exit(dev.set(old))

        onRejected(...)
      }
    },
    wrapSync = function(expr) {
      old <- dev.cur()
      dev.set(which)
      on.exit(dev.set(old))

      force(expr)
    }
  )
}
