#' Nearest neighbour based edge-lists
#'
#' `edge_nn` returns edge-lists defined by the nearest neighbour. 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.
#'
#' If a `threshold` is provided, it 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 edge nearest neighbours are calculated. The intended framework is to
#' group rows temporally with `group_times` then spatially with `edge_nn`. 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_nn` 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
#'
#' @param threshold (optional) spatial distance threshold to set maximum
#'   distance between an individual and their neighbour.
#' @inheritParams group_pts
#' @inheritParams edge_dist
#'
#' @return `edge_nn` returns a `data.table`  with three columns: timegroup, ID
#'   and NN. If 'returnDist' is TRUE, a column 'distance' is returned indicating
#'   the distance between ID and NN.  The ID and NN columns represent the edges
#'   defined by the nearest neighbours (and temporal thresholds with
#'   `group_times`).
#'
#'   If an individual was alone in a timegroup or splitBy, or did not have any
#'   neighbours within the threshold distance, they are assigned NA for nearest
#'   neighbour.
#'
#'   Note: unlike many other functions (eg. `group_pts`) in `spatsoc`, `edge_nn`
#'   needs to be reassigned. See details in
#'   [FAQ](https://docs.ropensci.org/spatsoc/articles/faq.html).
#'
#'
#' @export
#'
#' @family Edge-list generation
#' @family Distance 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"))
#'
#' # Select only individuals A, B, C for this example
#' DT <- DT[ID %in% c('A', 'B', 'C')]
#'
#' # 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_nn(DT, id = 'ID', coords = c('X', 'Y'),
#'         timegroup = 'timegroup')
#'
#' # Edge-list generation using maximum distance threshold
#' edges <- edge_nn(DT, id = 'ID', coords = c('X', 'Y'),
#'         timegroup = 'timegroup', threshold = 100)
#'
#' # Edge-list generation, returning distance between nearest neighbours
#' edge_nn(DT, id = 'ID', coords = c('X', 'Y'),
#'         timegroup = 'timegroup', threshold = 100,
#'         returnDist = TRUE)
#'
#' # Or, using the new geometry interface
#' get_geometry(DT, coords = c('X', 'Y'), crs = 32736)
#' edge_nn(DT, threshold = 100, id = 'ID', timegroup = 'timegroup', returnDist = TRUE)
edge_nn <- function(
    DT = NULL,
    id = NULL,
    coords = NULL,
    timegroup,
    crs = NULL,
    splitBy = NULL,
    threshold = NULL,
    geometry = 'geometry',
    returnDist = FALSE) {
  # NSE
  N <- geo <- x <- y <- NULL

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

  check_cols <- c(timegroup, id, 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?'
      )
    )
  }

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

  splitBy <- c(splitBy, timegroup)
  if (DT[, .N, by = c(id, splitBy)][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 (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)) {
      assert_threshold(threshold, crs)

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

    DT[, {

      distMatrix <- calc_distance(
        geometry_a = geo,
        use_dist = use_dist
      )
      diag(distMatrix) <- NA

      if (!is.null(threshold)) {
        distMatrix[distMatrix > threshold] <- NA
      }
      wm <- as.numeric(apply(distMatrix, MARGIN = 2, which.min))

      out_id <- id
      if (all(is.na(wm))) {
        out_nn <- NA_character_
        if (returnDist) {
          out_dist <- NA_real_
        }
      } else {
        out_nn <- id[wm]
        if (returnDist) {
          w <- wm + (length(wm) * (seq_along(wm) - 1))
          out_dist <- distMatrix[w]
        }
      }

      l <- list(ID = out_id,
                NN = out_nn)

      if (returnDist) {
        l <- c(l, list(distance = out_dist))
      }

      l
    },
    by = c(splitBy),
    env = list(geo = geometry, id = id)]
  } else {
    if (is.null(crs)) {
      crs <- sf::NA_crs_
    }

    assert_length(coords, 2)
    assert_are_colnames(DT, coords)
    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)) {
      assert_threshold(threshold, crs)

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

    DT[, {

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

      if (!is.null(threshold)) {
        distMatrix[distMatrix > threshold] <- NA
      }
      wm <- as.numeric(apply(distMatrix, MARGIN = 2, which.min))

      out_id <- id
      if (all(is.na(wm))) {
        out_nn <- NA_character_
        if (returnDist) {
          out_dist <- NA_real_
        }
      } else {
        out_nn <- id[wm]
        if (returnDist) {
          w <- wm + (length(wm) * (seq_along(wm) - 1))
          out_dist <- distMatrix[w]
        }
      }

      l <- list(ID = out_id,
                NN = out_nn)

      if (returnDist) {
        l <- c(l, list(distance = out_dist))
      }

      l
    },
    by = c(splitBy),
    env = list(x = xcol, y = ycol, id = id)]
  }


}

