# Constructor -------------------------------------------------------------

#' Guide primitives: segments
#'
#' This function constructs a [guide primitive][guide-primitives].
#'
#' @param key A [segment key][key_segments] specification. See more information
#'   in the linked topic. Alternatively, an object of class
#'   [`<hclust>`][stats::hclust] that automatically invokes `key_dendro()`.
#' @param space Either a [`<unit>`][grid::unit()] or [`<rel>`][ggplot2::rel()]
#'   object of length 1 determining the space allocated in the orthogonal
#'   direction. When the `space` argument is of class `<rel>` (default) the
#'   base size is taken from the tick length theme setting.
#' @param vanish Only relevant when the guide is used in the secondary theta
#'   position: a `<logical[1]>` on whether the continue to draw the segments
#'   until they meed in the center (`TRUE`) or strictly observe the `space`
#'   setting (`FALSE`).
#' @inheritParams common_parameters
#'
#' @return A `<PrimitiveSegments>` primitive guide that can be used inside other
#'   guides.
#' @family primitives
#' @export
#'
#' @details
#' ## Styling options
#'
#' Below are the [theme][ggplot2::theme] options that determine the styling of
#' this guide, which may differ depending on whether the guide is used in
#' an axis or in a legend context.
#'
#' The possible `{position}` suffixes mentioned below are `x`, `x.top`,
#' `x.bottom`, `y`, `y.left`, `y.right`. The `theta` and `r` position suffixes
#' in \pkg{ggplot2} are *not* obeyed in \pkg{legendry}.
#'
#' | **Theme setting** | **Context** | **Type** | **Description** |
#' | ----------------- | ----------- | -------- | --------------- |
#' | `axis.ticks.{position}` | Axis | [`element_line()`] | The line segments. |
#' | `axis.ticks.length.{position}` | Axis | [`unit()`] | Basis for the `space` argument |
#' | `legend.ticks` | Legend | [`element_line()`] | The line segments |
#' | `legend.ticks.length` | Legend | [`unit()`] | Basis for the `space` argument |
#'
#' Styling options *per segment* can be set in the [segment key][key_segments].
#' The `line` prefixed properties are prioritised for segments. These override
#' theme settings.
#'
#' The context-agnostic alternative to using `theme()` is to use
#' [`theme_guide()`]:
#'
#' ```r
#' primitive_segments(theme = theme_guide(
#'   ticks = element_line(),
#'   ticks.length = unit(5, "mm")
#' ))
#' ```
#'
#' @examples
#' # Building a key
#' key <- key_segment_manual(
#'   value     = c(1.6, 1.6, 3.4, 5.2),
#'   value_end = c(7.0, 7.0, 3.4, 5.2),
#'   oppo      = c(1.0, 2.0, 0.0, 0.0),
#'   oppo_end  = c(1.0, 2.0, 3.0, 3.0)
#' )
#'
#' # Using the primitive in a plot
#' ggplot(mpg, aes(displ, hwy)) +
#'   geom_point() +
#'   scale_x_continuous(
#'     guide = primitive_segments(key = key)
#'   )
primitive_segments <- function(key = NULL, space = rel(10.0), vanish = FALSE,
                               theme = NULL, position = waiver()) {
  check_unit(space, allow_rel = TRUE)

  if (inherits(key, "hclust")) {
    key <- key_dendro(key)
  }

  new_guide(
    key = key,
    space = space,
    vanish = vanish,
    theme = theme,
    position = position,
    available_aes = c("any", "x", "y", "r", "theta"),
    super = PrimitiveSegments
  )
}

# Class -------------------------------------------------------------------

PrimitiveSegments <- ggproto(
  "PrimitiveSegments", Guide,

  params = new_params(key = NULL, space = rel(10.0), vanish = FALSE),

  hashables = exprs(key),

  elements = list(
    position = list(line = "axis.ticks",   size = "axis.ticks.length"),
    legend   = list(line = "legend.ticks", size = "legend.ticks.length")
  ),

  extract_key = segment_extract_key,

  extract_params = primitive_extract_params,

  transform = function(self, params, coord, panel_params) {
    key <- params$key
    position <- params$position
    aesthetic <- params$aesthetic
    mult <- 10.0

    opposite <- setdiff(c("x", "y"), aesthetic)
    is_radius <- "theta.range" %in% names(panel_params) & !is_theta(position)
    if (is_radius) {
      range <- panel_params$r.range
      value <- squish_infinite(key[[aesthetic]], range)
      value <- rescale(value, panel_params$inner_radius, range)
      value <- rescale(value + 0.5, from = panel_params$bbox$x)
      key[[aesthetic]] <- value
      if (position == "left") {
        key[[opposite]] <- 1.0 - key[[opposite]]
      }
      if (aesthetic == "x") {
        key <- rename(key, c("x", "y"), c("y", "x"))
      }
      params$key <- key
      return(params)
    }

    range <- switch(
      position,
      top = , bottom = "y.range",
      left = , right = "x.range",
      theta = , theta.sec = "r.range"
    )
    range <- panel_params[[range]]

    margin_lower <- function(value) range[1L] - value * diff(range) / mult
    margin_upper <- function(value) range[2L] + value * diff(range) / mult

    key[[opposite]] <- switch(
      position,
      bottom = , theta.sec = , left = margin_lower(key[[opposite]]),
      top = , theta = , right = margin_upper(key[[opposite]]),
      key[[opposite]]
    )

    key <- coord_munch(coord, key, panel_params)

    key[[opposite]] <- switch(
      position,
      left = , bottom = key[[opposite]] * mult + 1.0,
      top = , right   = key[[opposite]] * mult - mult,
      key[[opposite]]
    )

    if (!is_theta(position)) {
      return(vec_assign(params, "key", list(key)))
    }

    radius <- panel_params$inner_radius
    if (position == "theta") {
      key$adjust <- (key$r - radius[2L]) * (2.0 * mult / radius[2L])
      key$r <- radius[2L]
    } else {
      key$adjust <- (key$r - radius[1L]) / diff(radius) * -mult
      key$r <- radius[1L]
    }
    bbox <- panel_params$bbox
    key$x <- rescale(key$r * sin(key$theta) + 0.5, from = bbox$x)
    key$y <- rescale(key$r * cos(key$theta) + 0.5, from = bbox$y)
    params$center <- c(rescale(0.5, from = bbox$x), rescale(0.5, from = bbox$y))

    params$key <- key
    params
  },

  setup_params = primitive_setup_params,
  setup_elements = primitive_setup_elements,
  override_elements = function(params, elements, theme) {
    size <- params$space
    if (is_rel(size)) {
      size <- unclass(size) * elements$size
    }
    elements$size <- convertUnit(size, "cm", valueOnly = TRUE)
    elements
  },

  build_ticks = function(key, elements, params, position = params$position) {
    if (is_empty(key)) {
      return(zeroGrob())
    }

    x <- unit(key$x, "npc")
    y <- unit(key$y, "npc")

    if (is_theta(position)) {
      vanish <- position == "theta.sec" && isTRUE(params$vanish)
      theta <- key$theta + as.numeric(position == "theta.sec") * pi
      offset <- elements$offset

      if (!vanish) {
        offset <- key$adjust * elements$size + offset
      }
      if (any(offset != 0.0)) {
        x <- x + unit(sin(theta) * offset, "cm")
        y <- y + unit(cos(theta) * offset, "cm")
      }
      if (vanish) {
        cx <- unit(params$center[1L] * key$adjust, "npc")
        cy <- unit(params$center[2L] * key$adjust, "npc")
        x <- (x * (1.0 - key$adjust)) + cx
        y <- (y * (1.0 - key$adjust)) + cy
      }
    }

    group <- new_rle(key$group)
    props <- element_key_properties(vec_slice(key, group$start), "line")

    inject(element_grob(
      elements$line, x = x, y = y,
      id.lengths = group$times,
      !!!props
    ))
  },

  draw = function(self, theme, position = NULL, direction = NULL,
                  params = self$params) {

    params <- replace_null(params, position = position, direction = direction)
    params <- self$setup_params(params)

    elems <- self$setup_elements(params, self$elements, theme)
    elems <- self$override_elements(params, elems, theme)
    segments <- self$build_ticks(params$key, elems, params)

    primitive_grob(
      grob = segments,
      size = unit(elems$size, "cm"),
      position = params$position,
      name = "segments"
    )
  }
)
