# Standard keys -----------------------------------------------------------

#' Standard keys
#'
#' @description
#' These functions are helper functions for working with tick marks as keys
#' in guides. They all share the goal of creating a guide key, but have
#' different outcomes:
#'
#' * `key_auto()` is a function factory whose functions extract a typical
#'   key from major breaks in a scale.
#' * `key_manual()` uses user-provided vectors to make a key.
#' * `key_map()` makes mappings from a `<data.frame>` to make a key.
#' * `key_minor()` is a function factory whose functions also extract minor
#'   break positions for minor tick marks.
#' * `key_log()` is a function factory whose functions place ticks at intervals
#'   in log10 space.
#' * `key_none()` makes an empty key with no entries.
#'
#' @param aesthetic,value A vector of values for the guide to represent
#'   equivalent to the `breaks` argument in scales. The `aesthetic` will be
#'   mapped, whereas `value` will not. For most intents and purposes,
#'   `aesthetic` and `value` should be identical.
#' @param label A `<character>` or list of expressions to use as labels.
#' @param type A `<character>` vector representing the one of the break types:
#'   `"major"`, `"minor"` or `"mini"`. If `NULL` (default), all breaks are
#'   treated as major breaks.
#' @param data A `<data.frame>` or similar object coerced by
#'   [`fortify()`][ggplot2::fortify] to a `<data.frame>`, in which the `mapping`
#'   argument is evaluated.
#' @param ...
#' The `...` parameter has two purposes.
#' 1. In `key_map()` it is [`<data-masking>`][rlang::topic-data-mask]. A set of
#'   mappings similar to those provided to [`aes()`][ggplot2::aes], which will
#'   be evaluated in the `data` argument. These must contain `aesthetic`
#'   mapping.
#' 2. In other keys, `...` can be used to transfer graphical properties to the
#'   individual breaks of a guide. For example, using `colour = "blue"` will
#'   draw parts of the guides associated with breaks in blue. There is a shallow
#'   hierarchy in that `text_colour`, `line_colour`, `rect_colour` and
#'   `point_colour` are the specific properties for elements, but all inherit
#'   from the main `colour` setting. Likewise, `size`, `linewidth`, `linetype`
#'   and `fill` have specific variants for elements.
#' @param labeller A `<function>` that receives major breaks and returns
#'   formatted labels. For `key_log()`, `NULL` will default to
#'   [`scales::label_log()`] for strictly positive numbers and a custom labeller
#'   when negative numbers are included.
#' @param prescale_base A `<numeric[1]>` giving the base of logarithm to
#'   transform data manually. The default, `NULL`, will use the scale
#'   transformation to calculate positions. It is only advisable to set the
#'   `prescale_base` argument when the data have already been log-transformed.
#'   When using a log-transform in the scale or in
#'   [`coord_transform()`][ggplot2::coord_transform], the default `NULL` is
#'   recommended.
#' @param negative_small A `<numeric[1]>` setting the smallest absolute value
#'   that is marked with a tick in case the scale limits include 0 or negative
#'   numbers.
#' @param expanded A `<logical[1]>` determining whether the ticks should cover
#'   the entire range after scale expansion (`TRUE`, default), or be restricted
#'   to the scale limits (`FALSE`).
#' @param call A [call][rlang::topic-error-call] to display in messages.
#'
#' @name key_standard
#' @family keys
#' @return
#' For `key_auto()`, `key_minor()` and `key_log()` a function. For
#' `key_manual()` and `key_map()` a `<data.frame>` with the `<key_standard>`
#' class.
#'
#' @examples
#' # An example scale
#' template <- scale_x_continuous(limits = c(0, 10))
#'
#' # The auto, minor and log keys operate on scales
#' key_auto()(template)
#' key_minor()(template)
#'
#' # So does the log key
#' template <- scale_x_continuous(transform = "log10", limits = c(0.1, 10))
#' key_log()(template)
#'
#' # Providing custom values
#' key_manual(
#'   aesthetic = 1:5,
#'   label = c("one", "two", "three", "four", "five")
#' )
#'
#' # Values from a `<data.frame>`
#' key_map(ToothGrowth, aesthetic = unique(supp))
#'
#' # Empty key
#' key_none()
NULL

#' @rdname key_standard
#' @export
key_auto <- function(..., call = NULL) {
  call <- call %||% current_call()
  function(scale, aesthetic = NULL) {
    aesthetic <- aesthetic %||% scale$aesthetics[1L]
    df <- Guide$extract_key(scale, aesthetic)
    df <- data_frame0(df, !!!extra_args(...), .error_call = call)
    class(df) <- c("key_standard", "key_guide", class(df))
    df
  }
}

#' @rdname key_standard
#' @export
key_manual <- function(aesthetic, value = aesthetic,
                       label = as.character(value), type = NULL,
                       ..., call = NULL) {
  df <- data_frame0(
    aesthetic = aesthetic, value = value,
    label = label, type = type, !!!extra_args(...),
    .error_call = call %||% current_call()
  )
  check_columns(df, c("aesthetic", "value", "label"))
  df <- rename(df, c("value", "label", "type"), c(".value", ".label", ".type"))
  class(df) <- c("key_standard", "key_guide", class(df))
  df
}

#' @rdname key_standard
#' @export
key_map <- function(data, ..., call = NULL) {
  mapping <- aes(!!!enquos(...))

  df <- eval_aes(
    data, mapping,
    required = "aesthetic",
    optional = c("value", "label", .element_params),
    call = call %||% current_call(),
    arg_mapping = "mapping", arg_data = "data"
  )
  df$value <- df$value %||% df$aesthetic
  df$label <- df$label %||% as.character(df$aesthetic)
  check_columns(df, c("aesthetic", "value", "label"))

  df <- rename(
    df,
    c("value", "label", .label_params),
    c(".value", ".label", paste0(".", .label_params))
  )
  class(df) <- c("key_standard", "key_guide", class(df))
  df
}


#' @rdname key_standard
#' @export
key_minor <- function(..., call = NULL) {
  call <- call %||% current_call()
  dots <- extra_args(...)
  function(scale, aesthetic = NULL) {
    aesthetic <- aesthetic %||% scale$aesthetics[1L]
    df <- GuideAxis$extract_key(scale, aesthetic, minor.ticks = TRUE)
    df <- data_frame0(df, !!!dots, .error_call = call)
    class(df) <- c("key_standard", "key_guide", class(df))
    df
  }
}

#' @rdname key_standard
#' @export
key_log <- function(
  prescale_base = NULL, negative_small = 0.1, expanded = TRUE,
  labeller = NULL, ..., call = NULL
) {
  check_number_decimal(
    negative_small, min = 1e-100,
    allow_infinite = FALSE,
    allow_null = TRUE
  )
  check_bool(expanded)
  check_number_decimal(prescale_base, allow_infinite = FALSE, allow_null = TRUE)
  check_function(labeller, allow_null = TRUE)
  force(prescale_base)
  force(negative_small)
  force(expanded)
  dots <- extra_args(...)
  call <- call %||% current_call()
  function(scale, aesthetic = NULL) {
    log10_keys(
      scale = scale, aesthetic = aesthetic,
      prescale_base = prescale_base,
      negative_small = negative_small,
      expanded = expanded, extra_args = dots, call = call
    )
  }
}

#' @rdname key_standard
#' @export
key_none <- function() {
  new_data_frame(
    list(aesthetic = logical(), .value = logical(), .label = character()),
    n = 0L, class = c("key_standard", "key_guide")
  )
}

# Helpers -----------------------------------------------------------------

validate_key_types <- function(key, call = caller_env()) {
  types <- c("major", "minor", "mini")
  if (!all(key$.type %in% types)) {
    extra <- setdiff(unique(key$.type), types)
    cli::cli_warn(c(paste0(
      "Key types must be one of {.or {.val {types}}}, not ",
      "{.or {.val {extra}}}."
    ), i = "Unknown types are dropped."), call = call)
    key <- vec_slice(key, key$.type %in% types)
  }
  key
}

resolve_key <- function(x, allow_null = FALSE) {
  if (allow_null && is.null(x)) {
    return(NULL)
  }
  if (is.character(x)) {
    fun <- find_global(paste0("key_", x), env = global_env(),
                       mode = "function")
    if (is.function(fun)) {
      x <- fun()
    }
  }
  if (is.function(x)) {
    return(x)
  }
  if (inherits(x, "key_guide")) {
    return(x)
  }
  cli::cli_abort("Unknown key specification: {x}.")
}

is_key_string <- function(x) {
  if (!is_bare_string(x)) {
    return(FALSE)
  }
  fun <- find_global(paste0("key_", x), env = global_env(), mode = "function")
  is.function(fun)
}

log10_keys <- function(scale, aesthetic,
                       prescale_base = NULL,
                       negative_small = 0.1,
                       expanded = TRUE,
                       labeller = NULL,
                       extra_args = NULL,
                       call = caller_env()) {
  aesthetic <- aesthetic %||% scale$aesthetics[1L]
  if (scale$is_discrete()) {
    cli::cli_abort(
      "Cannot calculate logarithmic ticks for discrete scales.",
      call = call
    )
  }

  transform <- scale$get_transformation()
  if (!is.null(prescale_base)) {
    if (transform$name != "identity") {
      cli::cli_warn(paste0(
        "The {.arg prescale_base} argument will override the scale's ",
        "{.field {transform$name}} transformation in log-tick positioning."
      ), call = call)
    }
    transform <- transform_log(base = prescale_base)
  }

  limits <- transform$inverse(scale$get_limits())
  has_negatives <- any(limits <= 0.0)

  if (has_negatives) {
    negative_small <- negative_small %||% 0.1
    start <- floor(log10(abs(negative_small)))
    end   <- ceiling(log10(max(abs(limits)))) + 1L
  } else {
    start <- floor(log10(min(limits))) - 1L
    end   <- ceiling(log10(max(limits))) + 1L
  }

  tens  <- 10L^seq(start, end, by = 1L)
  fives <- tens * 5L
  ones  <- as.vector(outer(setdiff(2L:9L, 5L), tens))

  if (has_negatives) {
    tens  <- tens[tens >= negative_small]
    tens  <- c(tens, -tens, 0.0)
    fives <- fives[fives >= negative_small]
    fives <- c(fives, -fives)
    ones  <- ones[ones >= negative_small]
    ones  <- c(ones, -ones)
    labeller <- labeller %||% negative_log_label
  }
  labeller <- labeller %||% scales::label_log()
  labels <- labeller(tens)

  ticks  <- transform$transform(c(tens, fives, ones))
  nticks <- c(length(tens), length(fives), length(ones))

  if (is.expression(labels)) {
    labels <- as.list(labels)
    labels <- pad(labels, sum(nticks), fill = list(NULL))
  } else {
    labels <- pad(labels, sum(nticks))
  }

  key <- data_frame0(
    !!aesthetic := ticks,
    .value = ticks,
    .label = labels,
    .type = rep(c("major", "minor", "mini"), times = nticks),
    !!!extra_args
  )

  if (expanded) {
    range <- scale$continuous_range %||% scale$get_limits()
  } else {
    range <- scale$get_limits()
  }
  key <- vec_slice(key, !is_oob(ticks, range))
  class(key) <- c("key_standard", "key_guide", class(key))
  key
}

negative_log_label <- function(x) {
  if (length(x) == 0L) {
    return(expression)
  }
  sign <- as.character(sign(x))
  sign[sign == "-1"] <- "-"
  sign[sign == "1"] <- "+"
  sign[sign == "0"] <- ""

  abs <- abs(x)
  exponent <- format(log(abs, base = 10.0), digits = 3L)
  text <- paste0(sign, 10L, "^", exponent)
  text[x == 0.0] <- "0"

  out <- vector("expression", length(text))
  for (i in seq_along(text)) {
    expr <- parse(text = text[[i]])
    out[[i]] <- if (length(expr) == 0L) NA else expr[[1L]]
  }
  out[is.na(x)] <- NA
  out
}

transform_key <- function(key, position, coord, panel_params) {
  if (is_empty(key)) {
    return(key)
  }
  other <- switch(position, bottom = , left = , theta.sec = -Inf, Inf)
  key <- replace_null(key, x = other, y = other)
  transformed <- coord$transform(key, panel_params)

  if (is_theta(position)) {
    add <- if (position == "theta.sec") pi else 0.0
    transformed$theta <- transformed$theta + add
  } else if ("theta" %in% setdiff(names(transformed), names(key))) {
    # For radius axes, we want to keep any `theta` value it had before,
    # but discard these when the coord forces this.
    transformed[c("theta", "r")] <- NULL
  }

  ends <- c("xend", "yend") %in% names(key)
  if (!any(ends)) {
    return(transformed)
  }

  if (ends[1L]) {
    key <- rename(key, c("x", "xend"), rev)
  }
  if (ends[2L]) {
    key <- rename(key, c("y", "yend"), rev)
  }
  key <- coord$transform(key, panel_params)
  if (is_theta(position)) {
    transformed$thetaend <- key$theta + add
  } else {
    if (ends[1L]) {
      transformed$xend <- key$x
    }
    if (ends[2L]) {
      transformed$yend <- key$y
    }
  }

  transformed
}
