#' Distance to group centroid
#'
#' `distance_to_centroid` calculates the distance of each relocation to the
#' centroid of the spatiotemporal group identified by `group_pts`. The function
#' expects a `data.table` with relocation data appended with a `group` column
#' from `group_pts` and centroid columns from `centroid_group`. Relocation data
#' should be provided in two columns representing the X and Y coordinates, or in
#' a geometry column prepared by the helper function [get_geometry()].
#'
#' 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()].
#'
#' This function expects a `group` column present generated with the `group_pts`
#' function and centroid coordinate column(s) generated with the
#' `centroid_group` function. The `group` arguments expect the names of columns
#' in `DT` which correspond to the group column. The `return_rank` argument
#' controls if the rank of each individual's distance to the group centroid is
#' also returned. 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 and under
#' "Distance function" for details on underlying distance function used.
#'
#' @section Interface:
#'  Two interfaces are available for providing coordinates:
#'
#'  1. Provide `coords` and `crs`. The `coords` argument expects the names of
#'  the X and Y coordinate columns. The `crs` argument expects a character
#'  string or numeric defining the coordinate reference system to be passed to
#'  [sf::st_crs]. For example, for UTM zone 36S (EPSG 32736), the crs argument
#'  is `crs = "EPSG:32736"` or `crs = 32736`. See <https://spatialreference.org>
#'  for a list of EPSG codes.
#'  2. (New!) Provide `geometry`. The `geometry` argument allows the user to
#'  supply a `geometry` column that represents the coordinates as a simple
#'  feature geometry list column. This interface expects the user to prepare
#'  their input DT with [get_geometry()]. To use this interface, leave the
#'  `coords` and `crs` arguments `NULL`, and the default argument for `geometry`
#'  ('geometry') will be used directly.
#'
#' @section Distance function:
#'
#'  The underlying distance function used depends on the crs of the coordinates
#'  or geometry provided.
#'
#'  - If the crs is longlat degrees (as determined by
#'  [sf::st_is_longlat()]), the distance function is [sf::st_distance()] which
#'  passes to [s2::s2_distance()] if [sf::sf_use_s2()] is TRUE and
#'  [lwgeom::st_geod_distance()] if [sf::sf_use_s2()] is FALSE. The distance
#'  returned has units set according to the crs.
#'
#'  - If the crs is not longlat degrees (eg. NULL, NA_crs_, or projected), the
#'  distance function used is [stats::dist()], maintaining expected behaviour
#'  from previous versions. The distance returned does not have units set.
#'
#'  Note: in both cases, if the coordinates are NA then the result will be NA.
#'
#' @param DT input data.table with centroid columns generated by eg.
#'   `centroid_group`
#' @param group group column name, generated by `group_pts`, default
#'   'group'
#' @param return_rank logical if rank distance should also be returned, default
#'   TRUE
#' @param ties.method see [`?data.table::frank()`][data.table::frank]
#' @inheritParams group_pts
#' @inheritParams direction_step
#'
#' @return `distance_to_centroid` returns the input `DT` appended with a
#'   `distance_centroid` column indicating the distance to the group centroid
#'   and, optionally, a `rank_distance_centroid` column indicating the within
#'   group rank distance to the group centroid (if `return_rank = TRUE`).
#'
#'   A message is returned when `distance_centroid` and optional
#'   `rank_distance_centroid` 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).
#'
#' @export
#' @family Distance functions
#' @family Centroid functions
#' @seealso [centroid_group], [group_pts], [sf::st_distance()]
#' @references
#' See examples of using distance to group centroid:
#'  * \doi{doi:10.1016/j.anbehav.2021.08.004}
#'  * \doi{doi:10.1111/eth.12336}
#'  * \doi{doi:10.1007/s13364-018-0400-2}
#'
#' @examples
#' # Load data.table
#' library(data.table)
#' \dontshow{data.table::setDTthreads(1)}
#' # Read example data
#' DT <- fread(system.file("extdata", "DT.csv", package = "spatsoc"))
#' \dontshow{
#' # (Subset example data to reduce runtime on CRAN)
#' if (isFALSE(rlang::is_interactive())) {
#'   DT <- DT[as.Date(datetime) == '2017-01-17']
#' }
#' }
#' # Cast the character column to POSIXct
#' DT[, datetime := as.POSIXct(datetime, tz = 'UTC')]
#'
#' # Temporal grouping
#' group_times(DT, datetime = 'datetime', threshold = '20 minutes')
#'
#' # Spatial grouping with timegroup
#' group_pts(DT, threshold = 5, id = 'ID',
#'           coords = c('X', 'Y'), timegroup = 'timegroup')
#'
#' # Calculate group centroid
#' centroid_group(DT, coords = c('X', 'Y'), group = 'group')
#'
#' # Calculate distance to group centroid
#' distance_to_centroid(
#'   DT,
#'   coords = c('X', 'Y'),
#'   group = 'group',
#' )
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' group_pts(DT, threshold = 5, id = 'ID', timegroup = 'timegroup')
#' centroid_group(DT)
#' direction_to_centroid(DT)
distance_to_centroid <- function(
    DT = NULL,
    coords = NULL,
    group = 'group',
    crs = NULL,
    return_rank = TRUE,
    ties.method = NULL,
    geometry = 'geometry') {

  # Due to NSE notes in R CMD check
  geo <- cent <- x <- y <- x_centroid <- y_centroid <- NULL

  assert_not_null(DT)
  assert_is_data_table(DT)

  assert_not_null(return_rank)

  out <- 'distance_centroid'

  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_col <- 'centroid'
    assert_are_colnames(DT, centroid_col, ', did you run centroid_group?')
    assert_col_inherits(DT, centroid_col, 'sfc_POINT')

    if (out %in% colnames(DT)) {
      message(out, ' column will be overwritten by this function')
      data.table::set(DT, j = out, value = NULL)
    }
    crs <- sf::st_crs(DT[[geometry]])
    use_dist <- isFALSE(sf::st_is_longlat(crs)) || identical(crs, sf::NA_crs_)

    DT[, c(out) := calc_distance(
      geometry_a = geo,
      geometry_b = cent,
      use_dist = use_dist
    ),
    env = list(geo = geometry, cent = centroid_col)
    ]

  } else {
    if (is.null(crs)) {
      crs <- sf::NA_crs_
    }

    assert_are_colnames(DT, coords)
    assert_length(coords, 2)
    assert_col_inherits(DT, coords, 'numeric')

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

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

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

    use_dist <- isFALSE(sf::st_is_longlat(crs)) || identical(crs, sf::NA_crs_)

    DT[,
      c(out) := calc_distance(
        x_a = x,
        y_a = y,
        x_b = x_centroid,
        y_b = y_centroid,
        crs = crs,
        use_dist = use_dist
      ),
      env = list(
        x = xcol, y = ycol,
        x_centroid = xcol_centroid, y_centroid = ycol_centroid
      )
    ]

  }

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

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

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

  return(DT[])
}
