#' Distance based edge-lists
#'
#' `edge_dist` returns edge-lists defined by a spatial distance within the user
#' defined threshold. The function expects a `data.table` with relocation data,
#' individual identifiers and a threshold argument. The threshold argument is
#' used to specify the criteria for distance between points which defines a
#' group. 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 `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`, `timegroup` (and optional `splitBy`) arguments expect the names of
#' columns in `DT` which correspond to the individual identifier, and timegroup
#' (generated by `group_times`) and additional grouping columns.
#'
#' The `threshold` provided should match the units of the coordinates. The
#' `threshold` can be provided with units specified using the units package (eg.
#' `threshold = units::set_units(10, m)`) which will be checked against the
#' units of the coordinates using the `crs`. If units are not specified, the
#' `threshold` is assumed to be in the units of the coordinates.
#'
#' The `timegroup` argument is required to define the temporal groups within
#' which edges are calculated. The intended framework is to group rows
#' temporally with `group_times` then spatially with `edge_dist`. If you have
#' already calculated temporal groups without `group_times`, you can pass this
#' column to the `timegroup` argument. Note that the expectation is that each
#' individual will be observed only once per timegroup. Caution that
#' accidentally including huge numbers of rows within timegroups can overload
#' your machine since all pairwise distances are calculated within each
#' timegroup.
#'
#' The `splitBy` argument offers further control over grouping. 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_dist` will only consider rows within each `splitBy` subgroup.
#'
#' See below under "Interface" for details on providing coordinates and under
#' "Distance function" for details on underlying distance function used.
#'
#' @inheritSection distance_to_centroid Interface
#' @inheritSection distance_to_centroid Distance function
#'
#' @inheritParams group_pts
#' @inheritParams direction_step
#' @param returnDist logical indicating if the distance between individuals
#'   should be returned. If FALSE (default), only individual columns (and
#'   timegroup, splitBy columns if provided) are returned. If TRUE, a column
#'   "distance" is also returned indicating the distance between individuals in
#'   the units of the `crs`, or if `crs = NULL` no units are set.
#' @param fillNA logical indicating if NAs should be returned for individuals
#'   that were not within the threshold distance of any other. If TRUE, NAs are
#'   returned. If FALSE, only edges between individuals within the threshold
#'   distance are returned.
#'
#' @return `edge_dist` returns a `data.table` with columns ID1, ID2, timegroup
#'   (if supplied) and any columns provided in splitBy. If 'returnDist' is TRUE,
#'   column 'distance' is returned indicating the distance between ID1 and ID2.
#'
#'   The ID1 and ID2 columns represent the edges defined by the spatial (and
#'   temporal with `group_times`) thresholds.
#'
#'   Note: unlike many other functions (eg. `group_pts`) in `spatsoc`,
#'   `edge_dist` needs to be reassigned. See details in
#'   [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#' @export
#'
#' @family Edge-list generation
#' @family Distance functions
#' @seealso [sf::st_distance()]
#'
#' @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',
#'     crs = 32736,
#'     returnDist = TRUE,
#'     fillNA = TRUE
#'   )
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' edge_dist(DT, threshold = 100, id = 'ID', timegroup = 'timegroup', returnDist = TRUE)
edge_dist <- function(
    DT = NULL,
    threshold,
    id = NULL,
    coords = NULL,
    timegroup,
    crs = NULL,
    splitBy = NULL,
    geometry = 'geometry',
    returnDist = FALSE,
    fillNA = TRUE) {
  # due to NSE notes in R CMD check
  ID1 <- ID2 <- N <- geo <- x <- y <- NULL

  assert_not_null(DT)
  assert_is_data_table(DT)
  assert_not_missing(threshold)
  assert_not_null(id)
  assert_not_missing(timegroup)
  assert_not_null(timegroup)

  check_cols <- c(timegroup, id, coords, splitBy)
  assert_are_colnames(DT, check_cols)

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


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

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

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

    if (is.null(threshold)) {
      edges <- DT[, {
        distMatrix <- calc_distance(
          geometry_a = geo,
          use_dist = use_dist
        )
        diag(distMatrix) <- NA

        if (returnDist) {
          l <- data.table::data.table(
            ID1 = id[rep(seq_len(nrow(distMatrix)), ncol(distMatrix))],
            ID2 = id[rep(seq_len(ncol(distMatrix)), each = nrow(distMatrix))],
            distance = c(distMatrix)
          )[ID1 != ID2]
        } else {
          l <- data.table::data.table(
            ID1 = id[rep(seq_len(nrow(distMatrix)), ncol(distMatrix))],
            ID2 = id[rep(seq_len(ncol(distMatrix)), each = nrow(distMatrix))]
          )[ID1 != ID2]
        }
        l
      },
      by = splitBy,
      env = list(geo = geometry, id = id)]
    } else {
      assert_threshold(threshold, crs)

      if (!inherits(threshold, 'units') && !identical(crs, sf::NA_crs_) &&
          !use_dist) {
        threshold <- units::as_units(threshold, units(sf::st_crs(crs)$SemiMajor))
      }

      edges <- DT[, {
        distMatrix <- calc_distance(
          geometry_a = geo,
          use_dist = use_dist
        )
        diag(distMatrix) <- NA

        w <- which(distMatrix < threshold, arr.ind = TRUE)

        if (returnDist) {
          l <- list(ID1 = id[w[, 1]],
                    ID2 = id[w[, 2]],
                    distance = distMatrix[w])
        } else {
          l <- list(ID1 = id[w[, 1]],
                    ID2 = id[w[, 2]])
        }
        l
      },
      by = splitBy,
      env = list(geo = geometry, id = id)]
    }
  } else {
    if (is.null(crs)) {
      crs <- sf::NA_crs_
    }

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

    xcol <- data.table::first(coords)
    ycol <- data.table::last(coords)

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

    if (is.null(threshold)) {
      edges <- DT[, {
        distMatrix <- calc_distance(
          x_a = x,
          y_a = y,
          crs = crs,
          use_dist = use_dist
        )
        diag(distMatrix) <- NA

        if (returnDist) {
          l <- data.table::data.table(
            ID1 = id[rep(seq_len(nrow(distMatrix)), ncol(distMatrix))],
            ID2 = id[rep(seq_len(ncol(distMatrix)), each = nrow(distMatrix))],
            distance = c(distMatrix)
          )[ID1 != ID2]
        } else {
          l <- data.table::data.table(
            ID1 = id[rep(seq_len(nrow(distMatrix)), ncol(distMatrix))],
            ID2 = id[rep(seq_len(ncol(distMatrix)), each = nrow(distMatrix))]
          )[ID1 != ID2]
        }
        l
      },
      by = splitBy,
      env = list(x = xcol, y = ycol, id = id)]
    } else {

      assert_threshold(threshold, crs)

      if (!inherits(threshold, 'units') && !identical(crs, sf::NA_crs_) &&
          !use_dist) {
        threshold <- units::as_units(threshold, 'm')
      }

      edges <- DT[, {
        distMatrix <- calc_distance(
          x_a = x,
          y_a = y,
          crs = crs,
          use_dist = use_dist
        )
        diag(distMatrix) <- NA

        w <- which(distMatrix < threshold, arr.ind = TRUE)

        if (returnDist) {
          l <- list(ID1 = id[w[, 1]],
                    ID2 = id[w[, 2]],
                    distance = distMatrix[w])
        } else {
          l <- list(ID1 = id[w[, 1]],
                    ID2 = id[w[, 2]])
        }
        l
      },
      by = splitBy,
      env = list(id = id, x = xcol, y = ycol)]
    }
  }

  if (fillNA) {
    merge(edges,
          unique(DT[, .SD, .SDcols = c(splitBy, id)]),
          by.x = c(splitBy, 'ID1'),
          by.y = c(splitBy, id),
          all = TRUE)
  } else {
    return(edges)
  }
}

