#' Behavioural zones
#'
#' `edge_zones` returns edge-lists defined by behavioural zones (Couzin
#' 2002). The function expects a distance based edge-list generated by
#' `edge_dist` (optionally with directions measured by
#' `direction_step` and `edge_direction`), zone thresholds, labels
#' and (optionally) a blind volume.
#'
#' `edge_zones` uses interindividual distances, and optionally directions,
#' to assign neighboring individuals to a focal individual's behavioural zones.
#' The user provides zone thresholds (eg. 25 m, 100 m, 250 m) along with zone
#' labels (eg. zone of repulsion, zone of orientation, zone of attraction),
#' according to their objectives, study species and system. The optional blind
#' volume can be provided to define a range of interindividual directions that
#' correspond to the limits of the focal individual's perception.
#'
#' Two workflows for this function exist, depending on if the blind volume
#' argument is used:
#'
#' a) If the blind volume is not provided, simply provide your distance based
#' edge-lists from `edge_dist` with zone thresholds and labels.
#'
#' b) If the blind volume is provided, the following order of functions is
#' expected to ensure the relevant direction columns are available:
#'    1. direction_step(DT)
#'    1. edges <- edge_dist(DT)
#'    1. dyad_id(edges)
#'    1. dyad_directions <- edge_direction(edges, DT)
#'    1. edge_zones(dyad_directions)
#'
#'  Interindividual distances are converted into behavioural zones using
#'  `cut`. The thresholds provided are used as cut points for a series of
#'  intervals that are open on the left and closed on the right, starting at 0.
#'  See details in [base::cut()].
#'
#'  The (optional) blind volume defines the range of interindividual directions
#'  between the focal individual (ID1) and the neighbour (ID2) that is outside
#'  of the focal individual's perception. The interindividual direction (column
#'  "direction_dyad" from `edge_direction`) is made relative to the focal
#'  individual's movement direction (column "direction" from
#'  `direction_step`). The argument blind_volume expects a single value to
#'  define a symmetrical window behind the focal individual's movement direction
#'  eg. where blind_volume = 2, the symmetrical window is from (-2, 2).
#'
#' The `edges` 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()].
#'
#' @param edges distance based edge-list generated by `edge_dist`,
#'   optionally with direction columns from `direction_step` and
#'   `edge_direction` (see Details)
#' @param zone_thresholds upper thresholds to define behavioural zones, eg.
#'   c(10, 20, 30) defines behavioural zones (0-10], (10-20], (20-30]
#' @param zone_labels labels for zones defined by zone_thresholds, must match
#'   zone_thresholds in length
#' @param blind_volume (optional) interindividual direction to define
#'   symmetrical window outside of focal individual's perception, eg. 2 becomes
#'   (-2, 2), see Details
#'
#' @return `edge_zones` returns the input `edges` appended with a zone
#'   column indicating the behavioural zone, using the zone label provided.
#'
#'   See details for appending outputs using modify-by-reference in the
#'   [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#'   A message is returned when a zone column already exists in the input
#'   `edges`, because it will be overwritten.
#'
#' @export
#' @references
#'
#' The behavioural zones metric is defined in Couzin et al. 2002
#' (\doi{doi:10.1006/jtbi.2002.3065}).
#'
#' See examples of measuring behavioural zones:
#'  * \doi{doi:10.1073/pnas.1001763107}
#'  * \doi{doi:10.1371/journal.pcbi.1008832}
#'  * \doi{doi:10.3389/fphy.2021.715996}
#'
#' @family Edge-list generating functions
#' @family Distance functions
#' @family Direction functions
#' @seealso [edge_dist] [direction_step] [edge_direction]
#' @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 = FALSE
#' )
#'
#' # Calculate behavioural zones
#' edge_zones(
#'   edges,
#'   zone_thresholds = c(25, 50, 75),
#'   zone_labels = c('repulsion', 'orientation', 'attraction')
#' )
#'
#' # Alternatively, if a user wants to specify a blind volume,
#' #   we need to measure directions
#' direction_step(
#'   DT = DT,
#'   id = 'ID',
#'   coords = c('X', 'Y'),
#'   crs = 32736
#' )
#'
#' # Edge list generation
#' edges <- edge_dist(
#'   DT,
#'   threshold = 100,
#'   id = 'ID',
#'   coords = c('X', 'Y'),
#'   timegroup = 'timegroup',
#'   returnDist = TRUE,
#'   fillNA = FALSE
#' )
#'
#' # Generate dyad id
#' dyad_id(edges, id1 = 'ID1', id2 = 'ID2')
#'
#' # Interindividual directions
#' dyad_directions <- edge_direction(
#'   edges,
#'   DT,
#'   id = 'ID',
#'   coords = c('X', 'Y'),
#'   crs = 32736,
#'   timegroup = 'timegroup'
#' )
#'
#' # Calculate behavioural zones
#' edge_zones(
#'   dyad_directions,
#'   zone_thresholds = c(25, 50, 75),
#'   zone_labels = c('repulsion', 'orientation', 'attraction'),
#'   blind_volume = 2
#' )
#' print(dyad_directions[, .SD[1:3], by = zone])
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' direction_step(DT, id = 'ID')
#' edges <- edge_dist(DT, threshold = 100, id = 'ID', timegroup = 'timegroup', returnDist = TRUE)
#' dyad_id(edges, id = 'ID1', id2 = 'ID2')
#' dyad_directions <- edge_direction(
#'   edges,
#'   DT,
#'   id = 'ID',
#'   timegroup = 'timegroup'
#' )
#' edge_zones(
#'   dyad_directions,
#'   zone_thresholds = c(25, 50, 75),
#'   zone_labels = c('repulsion', 'orientation', 'attraction'),
#'   blind_volume = 2
#' )
#' print(dyad_directions[, .SD[1:3], by = zone])
edge_zones <- function(
    edges = NULL,
    zone_thresholds = NULL,
    zone_labels = NULL,
    blind_volume = NULL) {
  # due to NSE notes in R CMD check
  distance <- direction_dyad <- direction <- direction_dyad_relative <- NULL
  direction <- 'direction'
  direction_dyad <- 'direction_dyad'
  distance <- 'distance'

  assert_not_null(edges)
  assert_is_data_table(edges)
  assert_not_null(zone_thresholds)
  assert_not_null(zone_labels)
  assert_are_colnames(edges, distance, ', did you run edge_dist?')
  assert_col_inherits(edges, distance, 'numeric')

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

  edges[, (out_col) := cut(distance, breaks = c(0, zone_thresholds),
                           labels = c(zone_labels))]

  if (!is.null(blind_volume)) {
    assert_are_colnames(edges, direction_dyad, ', did you use edge_direction?')
    assert_are_colnames(edges, direction, ', did you use direction_step?')
    assert_col_inherits(edges, direction_dyad, 'units', ', did you use direction_step?')
    assert_col_inherits(edges, direction, 'units', ', did you use direction_step?')
    assert_col_radians(edges, direction_dyad, ', did you use edge_direction?')
    assert_col_radians(edges, direction, ', did you use edge_direction?')

    edges[, direction_dyad_relative :=
            diff_rad(direction, direction_dyad, signed = TRUE)]

    edges[abs(direction_dyad_relative) > blind_volume & !is.na(get(out_col)),
          (out_col) := 'blind']

    data.table::set(edges, j = 'direction_dyad_relative', value = NULL)

  }

  return(edges)

}
