#' Direction based edge-lists
#'
#' [edge_direction()] returns edge lists defined by the direction between
#' individuals. The function expects a distance based edge-list generated by
#' `edge_nn` or [edge_dist()] and a `data.table` with relocation data appended
#' with a `timegroup` column from [group_times()]. It is required to use the
#' argument `fillNA = FALSE` for [edge_dist()] to ensure there are no NAs in the
#' coordinate columns. 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 `edges` and `DT` must be `data.table`s. 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 `edges` and `DT` are internally merged in this function using the columns
#' `id` / `ID1` and `ID2`, `timegroup`. The `id`, and `timegroup` arguments
#' expect the names of columns which correspond to the ID, and timegroup
#' columns in `DT`.
#'
#' See below under "Interface" for details on providing coordinates and under
#' "Direction function" for details on the underlying direction function used.
#'
#' @inheritSection direction_step Interface
#' @inheritSection direction_step Direction function
#'
#' @inheritParams centroid_dyad
#' @inheritParams direction_step
#' @inheritParams edge_alignment
#'
#' @references
#'
#' See examples of measuring the direction between individuals:
#'  * \doi{doi:10.1098/rsif.2013.0529}
#'  * \doi{doi:10.1098/rstb.2019.0380}
#'  * \doi{doi:10.1111/jfb.15315}
#'
#' @return [edge_direction()] returns the input `edges` appended with a
#'   "direction_dyad" column representing the direction between ID1 and ID2 in
#'   radians. A value of NaN is returned when the coordinates of ID1 equal the
#'   coordinates of ID2.
#'
#'   If the "direction" column is found in input DT, it will be retained for ID1
#'   in the output for use in downstream functions (eg. [edge_zones()]).
#'
#'   Missing values in coordinates / geometry are ignored and NA is returned.
#'
#'   Note: due to the merge required within this function, the output needs to
#'   be reassigned unlike some other `spatsoc` functions like [dyad_id()] and
#'   [group_pts()]. See details in
#'   [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#' @export
#' @family Edge-list generation
#' @family Direction functions
#' @seealso [dyad_id], [edge_dist], [edge_nn], [group_times],
#'   [lwgeom::st_geod_azimuth()]
#' @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
#'   )
#'
#' # Direction based edge-lists
#' dyad_directions <- edge_direction(
#'   edges,
#'   DT,
#'   id = 'ID',
#'   coords = c('X', 'Y'),
#'   crs = 32736,
#'   timegroup = 'timegroup'
#' )
#'
#' print(dyad_directions)
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' dyad_directions <- edge_direction(
#'   edges,
#'   DT,
#'   id = 'ID',
#'   timegroup = 'timegroup'
#' )
edge_direction <- function(
    edges = NULL,
    DT = NULL,
    id = NULL,
    coords = NULL,
    crs = NULL,
    timegroup = 'timegroup',
    geometry = 'geometry',
    projection = NULL) {

  # Due to NSE notes in R CMD check
  geo_id1 <- geo_id2 <- x_id1 <- y_id1 <- x_id2 <- y_id2 <- NULL

  if (!is.null(projection)) {
    warning('projection argument is deprecated, setting crs = projection')
    crs <- projection
  }

  assert_not_null(DT)
  assert_is_data_table(DT)
  assert_not_null(edges)
  assert_not_null(id)
  assert_not_null(timegroup)
  assert_are_colnames(DT, c(id, timegroup))
  assert_are_colnames(edges, timegroup)

  out_col <- 'direction_dyad'

  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')

    geometry_id1 <- paste0(geometry, '_id1')
    geometry_id2 <- paste0(geometry, '_id2')

    ID1_cols <- if ('direction' %in% colnames(DT)) {
      c('direction', geometry, id, timegroup)
    } else {
      c(geometry, id, timegroup)
    }

    if (geometry %in% colnames(edges)) {
      edges <- data.table::copy(edges)[, .SD, .SDcols = -c(geometry)]
    }

    m <- merge(edges,
               DT[, .SD, .SDcols = ID1_cols],
               by.x = c('ID1', timegroup),
               by.y = c(id, timegroup),
               all.x = TRUE,
               sort = FALSE)
    data.table::setnames(m, geometry, geometry_id1)
    m <- merge(m,
               DT[, .SD, .SDcols = c(geometry, id, timegroup)],
               by.x = c('ID2', timegroup),
               by.y = c(id, timegroup),
               all.x = TRUE,
               sort = FALSE)
    data.table::setnames(m, geometry, geometry_id2)

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

    crs <- sf::st_crs(DT[[geometry]])
    use_transform <- !sf::st_is_longlat(crs)

    if (is.na(use_transform)) {
      rlang::abort(paste0('sf::st_is_longlat(crs) is ', use_transform,
                          ', ensure crs is provided for direction functions'))
    }

    m[!sf::st_is_empty(geo_id1) & !sf::st_is_empty(geo_id2),
      (out_col) := calc_direction(
        geometry_a = geo_id1,
        geometry_b = geo_id2,
        use_transform = use_transform
      ),
      env = list(
        geo_id1 = geometry_id1,
        geo_id2 = geometry_id2
      )
    ]

    data.table::set(m, j = c(geometry_id1, geometry_id2), value = NULL)
    data.table::setcolorder(m, c(timegroup, 'ID1', 'ID2'))

  } else {
    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)
    coords_id1 <- paste0('id1_', coords)
    coords_id2 <- paste0('id2_', coords)

    ID1_cols <- if ('direction' %in% colnames(DT)) {
      c('direction', coords, id, timegroup)
    } else {
      c(coords, id, timegroup)
    }

    m <- merge(edges,
               DT[, .SD, .SDcols = ID1_cols],
               by.x = c('ID1', timegroup),
               by.y = c(id, timegroup),
               all.x = TRUE,
               sort = FALSE)
    data.table::setnames(m, coords, coords_id1)
    m <- merge(m,
               DT[, .SD, .SDcols = c(coords, id, timegroup)],
               by.x = c('ID2', timegroup),
               by.y = c(id, timegroup),
               all.x = TRUE,
               sort = FALSE)
    data.table::setnames(m, coords, coords_id2)

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

    use_transform <- !sf::st_is_longlat(crs)

    if (is.na(use_transform)) {
      rlang::abort(paste0('sf::st_is_longlat(crs) is ', use_transform,
                          ', ensure crs is provided for direction functions'))
    }

    m[!is.na(x_id1) & !is.na(y_id1) & !is.na(x_id2) & !is.na(y_id2),
      (out_col) := calc_direction(
        x_a = x_id1,
        y_a = y_id1,
        x_b = x_id2,
        y_b = y_id2,
        crs = crs,
        use_transform = use_transform
      ),
      env = list(
        x_id1 = data.table::first(coords_id1),
        y_id1 = data.table::last(coords_id1),
        x_id2 = data.table::first(coords_id2),
        y_id2 = data.table::last(coords_id2)
      )
    ]

    data.table::set(m, j = c(coords_id1, coords_id2), value = NULL)
    data.table::setcolorder(m, c(timegroup, 'ID1', 'ID2'))
  }



  return(m[])
}
