#' Fission-fusion id
#'
#' `fusion_id` identifies fusion events in distance based edge-lists.
#' The function expects a distance based edge-list generated by
#' `edge_dist`, a threshold argument and arguments controlling how fusion
#' events are defined.
#'
#' The `edges` must be a `data.table` returned by the `edge_dist`
#' function. In addition, `fusion_id` requires a dyad ID set on the edge
#' list generated by `dyad_id`. If your data is a `data.frame`, you
#' can convert it by reference using
#' [data.table::setDT()].
#'
#' The `threshold` must be provided in the units of the coordinates. The
#' `threshold` must be larger than 0. Eg. in the case of UTM, a `threshold` = 50
#' would indicate a 50 m distance threshold.
#'
#' The `n_min_length` argument defines the minimum number of successive
#' fixes that are required to establish a fusion event. The `n_max_missing`
#' argument defines the the maximum number of allowable missing observations for
#' the dyad within a fusion event. The `allow_split` argument defines if a
#' single observation can be greater than the threshold distance without
#' initiating fission event.
#'
#' @return `fusion_id` returns the input `edges` appended with a
#'   `fusionID` column.
#'
#'   This column represents the fusion event id. As with `spatsoc`'s
#'   grouping functions, the actual value of `fusionID` is arbitrary and
#'   represents the identity of a given fusion event. If the data was reordered,
#'   the `fusionID` may change, but the membership of each fusion event
#'   would not.
#'
#'   A message is returned when a column named `fusionID` already exists in
#'   the input `edges`, because it will be overwritten.
#'
#'   See details for appending outputs using modify-by-reference in the
#'   [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#' @param edges distance based edge-list generated by `edge_dist` function,
#'   with 'dyadID' column generated by `dyad_ID`
#' @param threshold spatial distance threshold in the units of the crs
#' @param n_min_length minimum length of fusion events
#' @param n_max_missing maximum number of missing observations within a fusion
#'   event
#' @param allow_split logical defining if a single observation can be greater
#'   than the threshold distance without initiating fission event
#' @export
#' @seealso `edge_dist`
#' @references
#' See examples of identifying fission-fusion events with spatiotemporal data:
#'  * \doi{doi:10.1111/ele.12457}
#'  * \doi{doi:10.1016/j.anbehav.2018.03.014}
#'  * \doi{doi:10.1890/08-0345.1}
#' @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')
#'
#' # Edge-list generation
#' edges <- edge_dist(
#'     DT,
#'     threshold = 100,
#'     id = 'ID',
#'     coords = c('X', 'Y'),
#'     timegroup = 'timegroup',
#'     returnDist = TRUE,
#'     fillNA = TRUE
#'   )
#'
#' dyad_id(edges, 'ID1', 'ID2')
#'
#' fusion_id(
#'   edges = edges,
#'   threshold = 100,
#'   n_min_length = 1,
#'   n_max_missing = 0,
#'   allow_split = FALSE
#'   )
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' edges <- edge_dist(DT, threshold = 100, id = 'ID', timegroup = 'timegroup', returnDist = TRUE)
#' dyad_id(edges, id = 'ID1', id2 = 'ID2')
#' fusion_id(edges, threshold = 100)
fusion_id <- function(
    edges = NULL,
    threshold = NULL,
    n_min_length = 0,
    n_max_missing = 0,
    allow_split = FALSE)  {

  # due to NSE notes  in R CMD check
  . <- both_run <- distance <- dyadID <- fusionID <- tg_diff <- timegroup <-
    within_dist_run <- is_lead <- within_dist <- NULL

  assert_not_null(edges)

  check_colnames <- c('dyadID', 'timegroup', 'distance')
  assert_are_colnames(edges, check_colnames)

  assert_not_null(threshold)
  assert_inherits(threshold, 'numeric')
  assert_relation(threshold, `>`, 0)

  assert_inherits(n_min_length, 'numeric')
  assert_inherits(n_max_missing, 'numeric')
  assert_inherits(allow_split, 'logical')

  unique_edges <- unique(edges[, .(dyadID, timegroup, distance)])

  data.table::setorder(unique_edges, 'timegroup')

  # Define timegroup threshold
  tg_threshold <- 1 + n_max_missing

  # Check if edge distance less than threshold
  unique_edges[, within_dist := distance < threshold]

  # If allow split, check if previously within threshold, and
  #   timegroup difference between before and after is only 1
  if (allow_split) {
    unique_edges[, within_dist := data.table::fifelse(
      within_dist | timegroup == min(timegroup) | timegroup == max(timegroup),
      within_dist,
      data.table::shift(within_dist, type = 'lag') &
        data.table::shift(within_dist, type = 'lead') &
        abs(timegroup - data.table::shift(timegroup, type = 'lag')) <= tg_threshold &
        abs(timegroup - data.table::shift(timegroup, type = 'lead')) <= tg_threshold
    ), by = dyadID]
  }

  # Runs of within
  unique_edges[, within_dist_run := data.table::rleid(within_dist), by = dyadID]
  unique_edges[!(within_dist), within_dist_run := NA_integer_]

  # Check timegroup difference, unless first obs for dyad
  unique_edges[, tg_diff := data.table::fifelse(
    within_dist,
    timegroup - data.table::shift(timegroup, type = 'lag') <= 1 |
      timegroup == min(timegroup),
    NA
  ), by = dyadID]

  # If missing obs allowed, adjust timegroup difference to allow as long as
  #   previously within threshold distance
  if (n_max_missing > 0) {
    unique_edges[, tg_diff := data.table::fifelse(
      tg_diff,
      tg_diff,
      data.table::shift(within_dist, type = 'lag') &
        (timegroup - data.table::shift(timegroup, type = 'lag')) <=
        (tg_threshold)
    ), by = dyadID]
  }

  # Get runs on within and timegroup difference.
  unique_edges[(within_dist), both_run := data.table::rleid(within_dist_run, tg_diff),
               by = dyadID]

  # Adjust if runs of isolated observations together
  #  (eg. within T, T but timegroup diff F, F)
  unique_edges[(within_dist) & !(tg_diff),
               both_run := (both_run + seq.int(.N)) * -1,
               by = dyadID]

  # Check if shift lead is fusion and within spatial+temporal threshold
  unique_edges[, is_lead :=
    both_run < 0 &
    abs(timegroup - data.table::shift(timegroup, type = 'lead')) <=
      tg_threshold &
      within_dist &
      data.table::shift(within_dist, type = 'lead'),
  by = dyadID
  ]

  # If shift lead is fusion then shift id to lead id
  unique_edges[, both_run := fifelse(
    is_lead,
    data.table::shift(both_run, type = 'lead'),
    both_run
  ),
  by = dyadID]

  # If n minimum length > 0, check nrows and return NA if less than min
  if (n_min_length > 0) {
    unique_edges[!is.na(both_run), both_run := data.table::fifelse(
      .N >= n_min_length,
      both_run,
      NA_integer_),
      by = .(dyadID, both_run)]
  }

  # Set fusion id on runs and dyad id
  unique_edges[!is.na(both_run), fusionID := .GRP, by = .(dyadID, both_run)]

  # Merge fusion id onto input edges
  if ('fusionID' %in% colnames(edges)) {
    message('fusionID column will be overwritten by this function')
    data.table::set(edges, j = 'fusionID', value = NULL)
  }
  edges[unique_edges, fusionID := fusionID, on = .(timegroup, dyadID)]
  return(edges)
}
