#' Create a Word Cloud from Term Frequencies
#'
#' @description Create a word cloud from a term-frequency table and save it as a PNG
#'   file. The most frequent terms are rendered, laid out within the chosen shape.
#'   Requires the \pkg{htmltools}, \pkg{htmlwidgets}, \pkg{magick}, \pkg{webshot2},
#'   and \pkg{wordcloud2} packages. System dependencies are
#'   [ImageMagick](https://imagemagick.org/) (for displaying the PNG),
#'   [OptiPNG](https://optipng.sourceforge.net/) (for PNG compression), and a
#'   [Chrome](https://www.google.com/chrome/) or Chromium-based browser with
#'   support for the Chrome DevTools protocol. Use
#'   [chromote::find_chrome()] to locate the browser.
#'
#' @param x 'data.frame'.
#'   Term-frequency table containing at least the columns `term` and `freq`.
#'
#' @param max_terms 'integer' number.
#'   Maximum number of terms to include, taken in order of decreasing frequency.
#'   Defaults to 200.
#'
#' @param size 'numeric' number.
#'   Font-size scaling factor. Defaults to 1.
#'
#' @param shape 'character' string.
#'   Shape of the cloud. One of `"circle"` (the default), `"cardioid"`, `"diamond"`,
#'   `"triangle-forward"`, `"triangle"`, `"pentagon"`, or `"star"`.
#'
#' @param ellipticity 'numeric' number between 0 and 1.
#'   Degree of flattening applied to the shape. Defaults to 0.65.
#'
#' @param ...
#'   Additional arguments passed to [wordcloud2::wordcloud2()].
#'
#' @param width 'integer' number.
#'   Image width in pixels. Defaults to 910. Image height is derived from `width`
#'   and `ellipticity`.
#'
#' @param output 'character' string.
#'   Path to the output PNG file. Defaults to a temporary file.
#'
#' @param display 'logical' flag.
#'   Whether to display the saved PNG in a graphics window. Requires the \pkg{magick}
#'   package. Defaults to `FALSE`.
#'
#' @return Invisibly returns the file path to the word-cloud PNG.
#'
#' @author J.C. Fisher, U.S. Geological Survey, Idaho Water Science Center
#'
#' @export
#'
#' @seealso [`mine_text`] to build a term-frequency table.
#'
#' @examples
#' \dontrun{
#'   d <- wordcloud2::demoFreq |> head(n = 10)
#'   colnames(d) <- c("term", "freq")
#'   file <- make_wordcloud(d, display = interactive())
#'
#'   unlink(file)
#' }

make_wordcloud <- function(
  x,
  max_terms = 200,
  size = 1,
  shape = "circle",
  ellipticity = 0.65,
  ...,
  width = 910,
  output = NULL,
  display = FALSE
) {
  # check required packages
  pkgs <- c(
    "htmltools",
    "htmlwidgets",
    "magick",
    "utils",
    "webshot2",
    "wordcloud2"
  )
  installed <- vapply(pkgs, FUN = requireNamespace, FUN.VALUE = logical(1), quietly = TRUE)
  if (any(!installed)) {
    txt <- pkgs[!installed] |> sQuote(q = FALSE) |> paste(collapse = ", ")
    stop("Word cloud requires missing package(s): ", txt, call. = FALSE)
  }

  # check arguments
  checkmate::assert_data_frame(
    x,
    types = c("factor", "character", "integerish"),
    any.missing = FALSE,
    min.rows = 3,
    min.cols = 2
  )
  checkmate::assert_names(colnames(x), must.include = c("term", "freq"))
  checkmate::assert_count(max_terms, positive = TRUE)
  checkmate::assert_number(size, lower = 0, finite = TRUE)
  shape <- match.arg(
    shape,
    choices = c(
      "circle",
      "cardioid",
      "diamond",
      "triangle-forward",
      "triangle",
      "pentagon",
      "star"
    )
  )
  checkmate::assert_number(ellipticity, lower = 0, upper = 1, finite = TRUE)
  checkmate::assert_count(width, positive = TRUE)
  checkmate::assert_flag(display)
  if (is.null(output)) {
    output <- tempfile(fileext = ".png")
  }
  output <- normalizePath(output, winslash = "/", mustWork = FALSE)
  checkmate::assert_path_for_output(output, overwrite = TRUE, extension = "png")

  # keep the most frequent terms
  d <- x[order(x$freq, decreasing = TRUE), ]
  d <- utils::head(d, n = max_terms)

  # create the word-cloud HTML widget
  wc <- wordcloud2::wordcloud2(d, size = size, shape = shape, ellipticity = ellipticity, ...)

  # hide hover labels
  sty <- htmltools::HTML(".wcLabel {display: none;}")
  tag <- htmltools::tags$style(sty) |> htmltools::tags$head()
  wc <- htmlwidgets::prependContent(wc, tag)

  # save the widget to a temporary HTML file
  html <- tempfile(fileext = ".html")
  htmlwidgets::saveWidget(wc, html, selfcontained = FALSE)

  # ensure HTML artifacts are removed even if the screenshot step errors
  on.exit(
    unlink(
      list.files(
        path = dirname(html),
        pattern = sub("\\.html$", "", basename(html)),
        full.names = TRUE
      ),
      recursive = TRUE
    ),
    add = TRUE
  )

  # screenshot the HTML widget into the PNG file
  webshot2::webshot(
    url = html,
    file = output,
    vwidth = width,
    vheight = as.integer(width * ellipticity),
    cliprect = "viewport",
    delay = 10
  )

  # recompress the PNG
  suppressWarnings(
    system2(
      command = "optipng",
      args = c("-quiet", "-strip all", "-o7", shQuote(output))
    )
  )

  # optionally display the result
  if (display) {
    magick::image_read(output) |> plot()
  }

  invisible(output)
}
