#' Directional alignment based edge-lists
#'
#' `edge_alignment` returns edge-lists defined by directional alignment
#' (difference in movement direction) between individuals. The function expects
#' a `data.table` with relocation data and individual identifiers, a
#' direction column (generated by `direction_step`) and timegroup column
#' (generated by `group_times`).
#'
#' The `DT` must be a `data.table`. If your data is a
#' `data.frame`, you can convert it by reference using
#' [data.table::setDT()].
#'
#' The `id`, `direction`, `timegroup`, and optional `group`
#' and `splitBy` arguments expect the names of a column in `DT` which
#' correspond to the individual identifier, direction (generated by
#' `direction_step`), timegroup (generated by `group_times`), group
#' (generated by `group_pts`) and additional grouping columns.
#'
#' There are two approaches to spatially restricting the calculation of
#' directional alignment. The `group` argument can be used to pass the
#' output group column from `group_pts` to calculate direction alignment
#' within spatiotemporal groups. Alternatively, the output of
#' `edge_alignment` can be merged with the output of `edge_dist` to
#' compare the difference in direction to the distance between individuals.
#'
#' The `splitBy` argument offers further control over the calculation of
#' directional alignment.  If within your `DT`, you have multiple
#' populations, subgroups or other distinct parts, you can provide the name of
#' the column which identifies them to `splitBy`. `edge_alignment`
#' will only consider rows within each `splitBy` subgroup.
#'
#' @inheritParams edge_dist
#' @inheritParams direction_polarization
#' @inheritParams diff_rad
#' @param timegroup character string of timegroup column name, default "timegroup"
#' @param group (optional) character string of group column name, used to restrict
#' the calculation of directional alignment to within spatiotemporal groups
#' @param splitBy (optional) vector of column names indicating subgroups within
#'   which the direction alignment will be calculated
#'
#' @return `edge_alignment` returns a `data.table` with columns ID1, ID2,
#'   timegroup, and a 'direction_diff' column indicating the difference in direction
#'   between ID1 and ID2 in radians, along with any columns provided in splitBy.
#'
#'  Note: unlike many other functions (eg. `group_pts`) in `spatsoc`,
#'  `edge_alignment` needs to be reassigned. See details in
#'  [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#' @references
#'
#' See examples of using directional alignment:
#'  * \doi{doi:10.1098/rsif.2013.0529}
#'  * \doi{doi:10.1016/j.beproc.2021.104473}
#'  * \doi{doi:10.1126/science.aaa5099}
#'
#' @export
#'
#' @family Edge-list generation
#' @family Direction functions
#'
#' @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')]
#'
#' # Temporal grouping
#' group_times(DT, datetime = 'datetime', threshold = '20 minutes')
#'
#' # Calculate direction
#' direction_step(
#'   DT = DT,
#'   id = 'ID',
#'   coords = c('X', 'Y'),
#'   crs = 32736
#' )
#'
#' # Calculate directional alignment edge-list
#' align <- edge_alignment(
#'   DT,
#'   id = 'ID',
#'   signed = FALSE
#' )
#'
#' # Or, calculate directional alignment within spatiotemporal groups
#' group_pts(DT, threshold = 5, id = 'ID',
#'           coords = c('X', 'Y'), timegroup = 'timegroup')
#'
#' align_group <- edge_alignment(
#'   DT,
#'   id = 'ID',
#'   group = 'group',
#'   signed = FALSE
#' )
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' group_pts(DT, threshold = 5, id = 'ID', timegroup = 'timegroup')
#' direction_step(DT, id = 'ID')
#' align_group <- edge_alignment(
#'   DT,
#'   id = 'ID',
#'   group = 'group',
#'   signed = FALSE
#' )
edge_alignment <- function(
    DT = NULL,
    id = NULL,
    direction = 'direction',
    timegroup = 'timegroup',
    group = NULL,
    splitBy = NULL,
    signed = FALSE) {
  # due to NSE notes in R CMD check
  ID1 <- ID2 <- N <- direction_diff <- NULL

  assert_not_null(DT)
  assert_is_data_table(DT)

  assert_not_null(id)
  assert_not_null(direction)
  assert_not_null(timegroup)

  check_colnames <- c(id, direction, timegroup, group, splitBy)
  assert_are_colnames(DT, check_colnames)

  assert_col_radians(DT, direction, ', did you use direction_step?')

  assert_inherits(signed, 'logical')

  if (any(unlist(lapply(DT[, .SD, .SDcols = timegroup], class)) %in%
    c('POSIXct', 'POSIXlt', 'Date', 'IDate', 'ITime', 'character'))) {
    warning(
      strwrap(
        prefix = ' ',
        initial = '',
        x = 'timegroup provided is a date/time
        or character type, did you use group_times?'
      )
    )
  }

  splitBy <- c(splitBy, timegroup)
  if (DT[, .N, by = c(id, splitBy, timegroup)][N > 1, sum(N)] != 0) {
    warning(
      strwrap(
        prefix = ' ',
        initial = '',
        x = 'found duplicate id in a
        timegroup and/or splitBy -
        does your group_times threshold match the fix rate?'
      )
    )
  }

  if ('splitBy' %in% colnames(DT)) {
    warning(
      strwrap(x = 'a column named "splitBy" was found in your data.table,
              renamed to "split_by" to avoid confusion with the argument
              "splitBy"')
    )
    data.table::setnames(DT, 'splitBy', 'split_by')
  }

  edges <- DT[, {
      m <- outer(direction, direction, FUN = diff_rad, signed = signed)

      data.table::data.table(
          ID1 = id[rep(seq_len(nrow(m)), ncol(m))],
          ID2 = id[rep(seq_len(ncol(m)), each = nrow(m))],
          direction_diff = c(m)
      )[ID1 != ID2]
    },
    by = splitBy,
    env = list(id = id, direction = direction)
  ]

  edges[, direction_diff := units::as_units(direction_diff, 'rad')][]

  return(edges)
}

