#' Leadership along group direction
#'
#' Given the mean direction of a group of individuals, `leader_direction_group`
#' shifts the coordinate system to a new origin at the group centroid and
#' rotates the coordinate system by the mean direction to return each
#' individual's position along the mean direction, representing leadership in
#' terms of the front-back position in each group's mean direction. Relocation
#' data should be in two columns representing the X and Y coordinates, or in a
#' geometry column prepared by the helper function [get_geometry()].
#'
#' The function expects a `data.table` with relocation data appended with a
#' `group_direction` column from [direction_group()] and group centroid columns
#' from [centroid_group()].
#'
#' The `DT` must be a `data.table`. If your data is a `data.frame`, you can
#' convert it by reference using [data.table::setDT()] or by reassigning using
#' [data.table::data.table()].
#'
#' The `group_direction` argument expects the names of columns in `DT` which
#' correspond to the mean group direction generated by [direction_group()]. The
#' mean group direction column is expected in units of radians. The
#' `return_rank` argument controls if the rank of each individual's distance to
#' the group centroid is also returned. If `return_rank` is TRUE, the `group`
#' argument is required to specify the group column generated by [group_pts()].
#' The `ties.method` argument is passed to [data.table::frank()], see details at
#' [`?data.table::frank()`][data.table::frank].
#'
#' See below under "Interface" for details on providing coordinates.
#'
#' @inheritSection direction_step Interface
#'
#' @return `leader_direction_group` returns the input `DT` appended with a
#'   `position_group_direction` column indicating the position along the group
#'   direction in the units of the crs and, optionally when `return_rank =
#'   TRUE`, a `rank_position_group_direction` column indicating the ranked
#'   position along the group direction.
#'
#'   A message is returned when `position_group_direction` or
#'   `rank_position_group_direction` columns already exist in the input `DT`,
#'   because they will be overwritten.
#'
#'   See details for appending outputs using modify-by-reference in the
#'   [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#' @param DT input data.table with group direction columns generated by
#'   `direction_group` and centroid columns generated by
#'   `centroid_group`
#' @param group_direction group_direction column name generated using
#'   `direction_group`, default 'group_direction'
#' @inheritParams direction_step
#' @inheritParams distance_to_centroid
#' @inheritParams group_pts
#'
#' @export
#' @seealso `direction_group`, `centroid_group`
#' @family Leadership functions
#' @family Direction functions
#'
#' @references
#' See examples of measuring leadership along group direction (also called
#' forefront index):
#'  * \doi{doi:10.1371/journal.pone.0036567}
#'  * \doi{doi:10.1111/jfb.15315}
#'  * \doi{doi:10.1098/rspb.2021.0839}
#'
#' @examples
#' # Load data.table
#' library(data.table)
#' \dontshow{data.table::setDTthreads(1)}
#' # Read example data
#' DT <- fread(system.file("extdata", "DT.csv", package = "spatsoc"))
#'
#' # Cast the character column to POSIXct
#' DT[, datetime := as.POSIXct(datetime, tz = 'UTC')]
#' \dontshow{
#' # (Subset example data to reduce runtime on CRAN)
#' if (isFALSE(rlang::is_interactive())) {
#'   DT <- DT[as.Date(datetime) == '2017-01-17']
#' }
#' }
#' # Temporal grouping
#' group_times(DT, datetime = 'datetime', threshold = '20 minutes')
#'
#' # Spatial grouping with timegroup
#' group_pts(DT, threshold = 50, id = 'ID',
#'           coords = c('X', 'Y'), timegroup = 'timegroup')
#'
#' # Calculate direction at each step
#' direction_step(
#'   DT = DT,
#'   id = 'ID',
#'   coords = c('X', 'Y'),
#'   crs = 32736
#' )
#'
#' # Calculate group centroid
#' centroid_group(DT, coords = c('X', 'Y'))
#'
#' # Calculate group direction
#' direction_group(DT)
#'
#' # Calculate leader in terms of position along group direction
#' leader_direction_group(DT, coords = c('X', 'Y'), crs = 32736)
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' direction_step(DT = DT, id = 'ID')
#' centroid_group(DT)
#' direction_group(DT)
#' leader_direction_group(DT)
leader_direction_group <- function(
    DT = NULL,
    group_direction = 'group_direction',
    coords = NULL,
    group = 'group',
    crs = NULL,
    geometry = 'geometry',
    return_rank = TRUE,
    ties.method = NULL) {
  # Due to NSE notes
  geo <- cent <- group_dir <- x <- x_centroid <- y <- y_centroid <- pos <- NULL

  assert_not_null(DT)
  assert_is_data_table(DT)
  assert_not_null(return_rank)

  assert_are_colnames(DT, group_direction)
  assert_col_radians(DT, group_direction, ', did you use direction_group?')

  pos_col <- 'position_group_direction'

  if (is.null(coords)) {
    if (!is.null(crs)) {
      message('crs argument is ignored when coords are null, using geometry')
    }

    assert_are_colnames(DT, geometry, ', did you run get_geometry()?')
    assert_col_inherits(DT, geometry, 'sfc_POINT')

    centroid <- 'centroid'
    assert_are_colnames(DT, centroid, ', did you run get_geometry()?')
    assert_col_inherits(DT, centroid, 'sfc_POINT')

    crs_ud_unit <- sf::st_crs(DT[[geometry]])$ud_unit

    if (!identical(crs_ud_unit, units::as_units('m'))) {
      rlang::abort(
        paste0(
          'sf::st_crs(crs)$ud_unit is ',
          ifelse(is.null(crs_ud_unit), NULL, units(crs_ud_unit)),
          ', leader_direction_group requires planar coordinates in m'
        )
      )
    }

    if (pos_col %in% colnames(DT)) {
      message(
        pos_col, ' column will be overwritten by this function'
      )
      data.table::set(DT, j = pos_col, value = NULL)
    }

    DT[, c(pos_col) := {
      coords_geo <- sf::st_coordinates(geo)
      coords_cent <- sf::st_coordinates(cent)

      cos(units::drop_units(group_dir)) * (coords_geo[, 1] - coords_cent[, 1]) +
        sin(units::drop_units(group_dir)) * (coords_geo[, 2] - coords_cent[, 2])
    },
    env = list(
      group_dir = group_direction,
      geo = geometry,
      cent = centroid
    )
    ]

  } else {
    assert_not_null(coords)
    assert_are_colnames(DT, coords)
    assert_length(coords, 2)
    assert_col_inherits(DT, coords, 'numeric')
    assert_not_null(crs)

    xcol <- data.table::first(coords)
    ycol <- data.table::last(coords)
    pre <- 'centroid_'
    centroid_xcol <- paste0(pre, xcol)
    centroid_ycol <- paste0(pre, ycol)
    coords_centroid  <- c(centroid_xcol, centroid_ycol)

    assert_are_colnames(DT, coords_centroid, ', did you run centroid_group?')
    assert_col_inherits(DT, coords_centroid, 'numeric')

    crs_ud_unit <- sf::st_crs(crs)$ud_unit

    if (!identical(crs_ud_unit, units::as_units('m'))) {
      rlang::abort(
        paste0(
          'sf::st_crs(crs)$ud_unit is ',
          ifelse(is.null(crs_ud_unit), NULL, units(crs_ud_unit)),
          ', leader_direction_group requires planar coordinates in m'
        )
      )
    }

    if (pos_col %in% colnames(DT)) {
      message(
        pos_col, ' column will be overwritten by this function'
      )
      data.table::set(DT, j = pos_col, value = NULL)
    }

    DT[, c(pos_col) :=
      cos(units::drop_units(group_dir)) * (x - x_centroid) +
      sin(units::drop_units(group_dir)) * (y - y_centroid),
    env = list(
      group_dir = group_direction,
      x = xcol, y = ycol,
      x_centroid = centroid_xcol, y_centroid = centroid_ycol
    )
    ]
  }

  if (return_rank) {
    rank_col <- 'rank_position_group_direction'
    if (rank_col %in% colnames(DT)) {
      message(
        paste0(rank_col, ' column will be overwritten by this function')
      )
      data.table::set(DT, j = rank_col, value = NULL)
    }

    assert_not_null(group)
    assert_are_colnames(DT, group, ', did you run group_pts?')

    DT[, c(rank_col) :=
         data.table::frank(-pos, ties.method = ties.method,
                           na.last = 'keep'),
       by = c(group),
       env = list(pos = pos_col)]
  }

  return(DT[])
}
