#' Run an algorithm using 'qgis_process'
#'
#' Runs an algorithm using 'qgis_process'.
#' See the [QGIS docs](https://docs.qgis.org/latest/en/docs/user_manual/processing_algs/)
#' for a detailed description of the algorithms provided
#' 'out of the box' on QGIS.
#'
#' `qgis_run_algorithm()` accepts various R objects as algorithm arguments.
#' An overview is given by `vignette("qgis_arguments")`.
#' Examples include an R matrix or data frame for the
#' argument type 'matrix', R colors for the argument type 'color',
#' sf or terra (SpatVector) objects for the argument type 'vector' and
#' raster/terra/stars objects for the argument type 'raster', but there are many
#' more.
#' `qgis_run_algorithm()` preprocesses the provided objects into the format that
#' QGIS expects for a given argument.
#'
#' Providing R objects that cannot be converted to the applicable argument type
#' will lead to an error.
#'
#' Algorithm arguments can be passed as arguments of [qgis_run_algorithm()], but
#' they can also be combined as a JSON string and fed into the `.raw_json_input`
#' argument.
#' A JSON string can be obtained from the QGIS GUI, either from the
#' processing tool dialog or from the processing history dialog, by selecting
#' 'Copy as JSON' in the 'Advanced' dropdown menu.
#' So a user can first try out a geoprocessing step in the QGIS GUI, and
#' once the chosen algorithm arguments are satisfactory, copy the JSON string
#' to reproduce the operation in R.
#' A screenshot is available at the package homepage.
#'
#' @section Running QGIS models and Python scripts:
#' QGIS models and Python scripts can be added to the Processing Toolbox in the
#' QGIS GUI, by pointing at their corresponding file.
#' This will put the model or script below the provider 'Models' or
#' 'Scripts', respectively.
#' Next, it is necessary to run [qgis_configure()] in R in order to make the
#' model or script available to qgisprocess (even reloading the package won't
#' detect it, since these providers have dynamic content, not tied to a
#' plugin or to a QGIS version).
#' You can check the outcome with [qgis_providers()] and
#' [qgis_search_algorithms()].
#' Now, just as with other algorithms, you can provide the `model:<name>` or
#' `script:<name>` identifier to the `algorithm` argument of
#' `qgis_run_algorithm()`.
#'
#' As the output argument name of a QGIS model can have an R-unfriendly
#' syntax, you may need to take the JSON parameter string from the QGIS
#' processing dialog and feed the JSON string to the `.raw_json_input` argument
#' of `qgis_run_algorithm()` instead of providing separate arguments.
#'
#' Although the 'qgis_process' backend also supports replacing the 'algorithm'
#' parameter by the file path of a model file or a Python script, it is not
#' planned to implement this in qgisprocess, as it would bypass argument
#' preprocessing in R (including checks).
#'
#' @family functions to run one geoprocessing algorithm
#'
#' @seealso `vignette("qgis_arguments")`
#'
#' @param algorithm A qualified algorithm name (e.g., `"native:buffer"`) or
#'   a path to a QGIS model file.
#' @param PROJECT_PATH,ELLIPSOID Global values for QGIS project file and
#'   ellipsoid name for distance calculations.
#' @param ... Named key-value pairs as arguments for the algorithm. Features of
#'   [rlang::list2()] are supported. These arguments
#'   are converted to strings using [as_qgis_argument()].
#' @param .quiet Use `FALSE` to get extra output from 'qgis_process'.
#' This can be useful in debugging.
#' @param .raw_json_input The raw JSON to use as input in place of `...`.
#' See _Details_ section.
#'
#' @returns A `qgis_result` object.
#'
#' @examplesIf has_qgis()
#' qgis_run_algorithm(
#'   "native:buffer",
#'   INPUT = system.file("longlake/longlake_depth.gpkg", package = "qgisprocess"),
#'   DISTANCE = 10
#' )
#'
#' @export
qgis_run_algorithm <- function(algorithm, ..., PROJECT_PATH = NULL, ELLIPSOID = NULL,
                               .raw_json_input = NULL, .quiet = TRUE) {
  assert_qgis()
  assert_qgis_algorithm(algorithm)

  dots <- rlang::list2(...)
  if (length(dots) > 0 && !rlang::is_named(dots)) {
    abort("All ... arguments to `qgis_run_algorithm()` must be named.")
  }

  use_json_input <- !is.null(.raw_json_input) || qgis_using_json_input()
  use_json_output <- use_json_input || qgis_using_json_output()

  if (is.null(.raw_json_input)) {
    # sanitize arguments and make sure they are cleaned up on exit
    args <- qgis_sanitize_arguments(
      algorithm,
      !!!dots,
      PROJECT_PATH = PROJECT_PATH,
      ELLIPSOID = ELLIPSOID,
      .use_json_input = use_json_input
    )
    on.exit(qgis_clean_arguments(args))

    # generate command-line args or JSON input
    args_str <- qgis_serialize_arguments(args, use_json_input = use_json_input)
  } else {
    args_str <- .raw_json_input
    args <- list()
    if (length(list(...)) > 0) {
      abort("Can't use `.raw_json_input` with arguments in `qgis_run_algorithm()`")
    }
  }

  if (use_json_input) {
    stdin_file <- tempfile()
    on.exit(unlink(stdin_file), add = TRUE)
    writeLines(args_str, stdin_file, useBytes = TRUE)

    if (!.quiet) {
      cat("JSON input ----\n")
      cat(jsonlite::prettify(args_str, indent = 2))
      cat("\n")
    }
  }

  result <- qgis_run(
    args = c(
      if (use_json_output) "--json",
      arg_skip_loading_plugins(algorithm),
      "run",
      algorithm,
      if (use_json_input) "-" else args_str
    ),
    echo_cmd = !.quiet,
    stdout_callback = if (!.quiet && !use_json_output) function(x, ...) cat(x),
    stderr_callback = function(x, ...) message(x, appendLF = FALSE),
    stdin = if (use_json_input) stdin_file
  )

  if (!.quiet) cat("\n")

  # return a custom object to keep as much information as possible
  # about the output
  result <- structure(
    rlang::list2(
      !!!qgis_parse_results(algorithm, result$stdout),
      .algorithm = algorithm,
      .args = args,
      .raw_json_input = if (use_json_input) args_str,
      .processx_result = result
    ),
    class = "qgis_result"
  )

  qgis_check_stdout(result)
  result
}
