# Construct a Alpha complex
#
# S is a finite set of points in R^d. Using the Voronoi decomposition
# {V_s}_{s in S}, the alpha complex A_epsilon(S) is the nerve of the cover
# {V_s ∩ B(s, epsilon)}_{s in S} of S_epsilon = union_s B(s, epsilon); as
# epsilon -> Inf this cover tends to the Voronoi decomposition itself, and
# A_Inf(S) is exactly the Delaunay complex. Rather than computing that nerve directly, we use the
# standard equivalent computational recipe "Three-Dimensional Alpha Shapes"

#' @keywords internal
.alpha_values <- function(points, top_simplices) {
  points <- as.matrix(points)
  n <- nrow(points)
  d_top <- max(sapply(top_simplices, length)) - 1

  faces_by_dim <- lapply(0:d_top, function(k) faces(top_simplices, k))
  key_of <- function(s) paste(sort(s), collapse = "-")

  filt_val <- new.env(hash = TRUE, parent = emptyenv())

  # top dimension: always Gabriel by the Delaunay empty-circumsphere property
  for (s in faces_by_dim[[d_top + 1]]) {
    cs <- circumsphere(points[s, , drop = FALSE])
    assign(key_of(s), cs$radius, envir = filt_val)
  }

  if (d_top >= 1) {
    for (k in (d_top - 1):0) {
      cur_faces <- faces_by_dim[[k + 1]]
      higher_faces <- faces_by_dim[[k + 2]]

      # push each already-timed (k+1)-face's value down onto its own
      # k-dim sub-faces: a non-Gabriel sigma inherits min(alpha(tau)) over
      # its cofaces tau, and every coface has by now been timed.
      cand <- new.env(hash = TRUE, parent = emptyenv())
      for (t in higher_faces) {
        tval <- get(key_of(t), envir = filt_val)
        subfaces <- if (length(t) == 2) as.list(t) else {
          m <- utils::combn(t, length(t) - 1)
          lapply(seq_len(ncol(m)), function(j) m[, j])
        }
        for (sf in subfaces) {
          skey <- key_of(sf)
          prev <- mget(skey, envir = cand, ifnotfound = list(Inf))[[1]]
          if (tval < prev) assign(skey, tval, envir = cand)
        }
      }

      for (s in cur_faces) {
        cs <- circumsphere(points[s, , drop = FALSE])
        others <- setdiff(seq_len(n), s)
        gabriel <- TRUE
        if (length(others) > 0) {
          dd <- sqrt(rowSums(sweep(points[others, , drop = FALSE], 2, cs$center)^2))
          tol <- 1e-8 * max(1, cs$radius)
          if (any(dd < cs$radius - tol)) gabriel <- FALSE
        }
        val <- if (gabriel) cs$radius else get(key_of(s), envir = cand)
        assign(key_of(s), val, envir = filt_val)
      }
    }
  }

  list(faces_by_dim = faces_by_dim, filt_val = filt_val, key_of = key_of)
}

#' Construct an Alpha Complex
#'
#' @param points A numeric matrix or data.frame with one point per row
#'   (columns are coordinates); must be in "general position" (see
#'   \code{\link{circumsphere}}).
#' @param epsilon The alpha-complex scale. A simplex is included once its
#'   alpha value is at most \code{epsilon}. Defaults to \code{Inf}, which
#'   gives the full Delaunay complex (see \code{\link{DelaunayComplex}}).
#' @return A list of class \code{"alpha_complex"} with elements:
#' \describe{
#'   \item{simplices}{A list of integer vectors, every included simplex (all
#'     dimensions, not just the maximal ones - membership in the alpha
#'     complex is not simply generated by cliques).}
#'   \item{filtration}{A numeric vector, the alpha value of each simplex in
#'     \code{simplices}, in the same order.}
#' }
#' Pass the result to \code{\link{as_filtration}} for the same
#' \code{list(simplex=, t=)} format used by \code{\link{build_filtration}}
#' and every persistence function, or just call
#' \code{build_filtration(points, method = "Alpha", eps_max = epsilon)}.
#'
#' @importFrom geometry delaunayn
#' @export
#' @examples
#' points <- matrix(c(0, 1, 1, 0, 0, 0, 1, 1), ncol = 2)
#' alpha_complex <- AlphaComplex(points, epsilon = 1)
AlphaComplex <- function(points, epsilon = Inf) {
  points <- as.matrix(points)
  delaunay <- geometry::delaunayn(points, options = "Qt Qbb Qc Qz")
  top_simplices <- lapply(seq_len(nrow(delaunay)), function(i) sort(delaunay[i, ]))

  av <- .alpha_values(points, top_simplices)
  all_faces <- unlist(av$faces_by_dim, recursive = FALSE)
  values <- vapply(all_faces, function(s) get(av$key_of(s), envir = av$filt_val), 0)

  keep <- values <= epsilon
  structure(
    list(simplices = all_faces[keep], filtration = values[keep]),
    class = "alpha_complex"
  )
}

#' Construct a Delaunay Complex
#'
#' @param points A numeric matrix or data.frame with one point per row
#'   (columns are coordinates); must be in "general position".
#' @return A list of class \code{c("delaunay_complex", "alpha_complex")};
#'   see \code{\link{AlphaComplex}} for the element description.
#'
#' @export
#' @examples
#' points <- matrix(c(0, 1, 1, 0, 0, 0, 1, 1), ncol = 2)
#' delaunay_complex <- DelaunayComplex(points)
DelaunayComplex <- function(points) {
  result <- AlphaComplex(points, epsilon = Inf)
  class(result) <- c("delaunay_complex", "alpha_complex")
  result
}
