#' Compare Two Networks from Sequence Data using a Permutation Test
#'
#' This function compares two networks built from sequence data using
#' permutation tests. The function builds Markov models for two sequence
#' objects, computes the transition probabilities, and compares them by
#' performing permutation tests. It returns the differences in transition
#' probabilities, effect sizes, estimated p-values, and confidence intervals.
#'
#' @export
#' @family validation
#' @param x A `tna` object containing sequence data for the first `tna` model.
#' @param y A `tna` object containing sequence data for the second `tna` model.
#' @param adjust A `character` string for the method to adjust p-values with
#' for multiple comparisons. The default is `"none"` for no adjustment.
#' See the `method` argument of [stats::p.adjust()] for details and available
#' adjustment methods.
#' @param iter An `integer` giving the number of permutations to perform.
#' The default is 1000.
#' @param paired A `logical` value. If `TRUE`, perform paired permutation tests;
#' if `FALSE`, perform unpaired tests. The default is `FALSE`.
#' @param level A `numeric` value giving the significance level for the
#' permutation tests. The default is 0.05.
#' @param measures A `character` vector of centrality measures to test.
#' See [centralities()] for a list of available centrality measures.
#' @param ... Additional arguments passed to [centralities()].
#' @return A `tna_permutation` object which is a `list` with two elements:
#' `edges` and `centralities`, both containing the following elements:
#'
#'   * `stats`: A `data.frame` of original differences, effect sizes, and
#'     estimated p-values for each edge or centrality measure. The effect size
#'     is computed as the observed difference divided by the standard deviation
#'     of the differences of the permuted samples.
#'   * `diffs_true`: A `matrix` of differences in the data.
#'   * `diffs_sig`: A `matrix` showing the significant differences.
#'
#' @examples
#' model_x <- tna(group_regulation[1:200, ])
#' model_y <- tna(group_regulation[1001:1200, ])
#' # Small number of iterations for CRAN
#' permutation_test(model_x, model_y, iter = 20)
#'
permutation_test <- function(x, ...) {
  UseMethod("permutation_test")
}

#' @export
#' @rdname permutation_test
permutation_test.tna <- function(x, y, adjust = "none", iter = 1000,
                                 paired = FALSE, level = 0.05,
                                 measures = character(0), ...) {
  check_missing(x)
  check_missing(y)
  check_tna_seq(x)
  check_tna_seq(y)
  check_values(iter, strict = TRUE)
  check_flag(paired)
  check_range(level, lower = 0, upper = 1)
  adjust <- check_match(adjust, stats::p.adjust.methods, match_case = TRUE)
  permutation_test_(
    x = x,
    y = y,
    adjust = adjust,
    iter = iter,
    paired = paired,
    level = level,
    measures = measures,
    ...
  )
}

#' Compare Networks using a Permutation Test
#'
#' Test edge weight differences between all pairs or a subset of pairs of
#' a `group_tna` object. See [permutation_test.tna()] for more details.
#'
#' @export
#' @family validation
#' @param x A `group_tna` object
#' @param groups An `integer` vector or a `character` vector of group indices
#' or names, respectively, defining which groups to compare. When not provided,
#' all pairs are compared (the default).
#' @param consecutive A `logical` value. If `FALSE` (the default), all pairwise
#' comparisons are performed in lexicographic order with respect to the order
#' of the groups. If `TRUE`, only comparisons between consecutive pairs
#' of groups are performed.
#' @inheritParams permutation_test.tna
#' @examples
#' model <- group_model(engagement_mmm)
#' # Small number of iterations for CRAN
#' permutation_test(model, iter = 20)
#'
permutation_test.group_tna <- function(x, groups, adjust = "none",
                                       iter = 1000, paired = FALSE,
                                       level = 0.05, measures = character(0),
                                       consecutive = FALSE, ...) {
  check_missing(x)
  check_class(x, "group_tna")
  stopifnot_(
    length(attr(x, "scaling")) == 0L || attr(x, "groupwise"),
    "Permutation test is not supported for
     grouped models with globally scaled edge weights."
  )
  check_values(iter, strict = TRUE)
  check_flag(paired)
  check_range(level, lower = 0, upper = 1)
  adjust <- check_match(adjust, stats::p.adjust.methods, match_case = TRUE)
  x_names <- names(x)
  groups <- groups %m% seq_along(x)
  check_cluster(x, groups)
  groups <- ifelse_(
    is.character(groups),
    which(x_names %in% groups),
    groups
  )
  n_groups <- length(groups)
  stopifnot_(
    n_groups >= 2L,
    "Argument {.arg groups} must contain at least two groups to compare."
  )
  if (consecutive) {
    out <- vector(mode = "list", length = n_groups - 1L)
    for (i in seq_len(n_groups - 1L)) {
      group_i <- groups[i]
      group_j <- groups[i + 1L]
      out[[i]] <- permutation_test_(
        x = x[[group_i]],
        y = x[[group_j]],
        adjust = adjust,
        iter = iter,
        paired = paired,
        level = level,
        measures = measures,
        ...
      )
      names(out)[i] <- paste0(x_names[group_i], " vs. ", x_names[group_j])
    }
  } else {
    n_pairs <- (n_groups * (n_groups - 1L)) %/% 2L
    out <- vector(mode = "list", length = n_pairs)
    idx <- 0L
    for (i in seq_len(n_groups - 1L)) {
      for (j in seq(i + 1L, n_groups)) {
        idx <- idx + 1L
        group_i <- groups[i]
        group_j <- groups[j]
        out[[idx]] <- permutation_test_(
          x = x[[group_i]],
          y = x[[group_j]],
          adjust = adjust,
          iter = iter,
          paired = paired,
          level = level,
          measures = measures,
          ...
        )
        names(out)[idx] <- paste0(x_names[group_i], " vs. ", x_names[group_j])
      }
    }
  }
  structure(
    out,
    class = "group_tna_permutation"
  )
}

permutation_test_ <- function(x, y, adjust, iter, paired, level,
                              measures, ...) {
  data_x <- x$data
  data_y <- y$data
  n_x <- nrow(data_x)
  n_y <- nrow(data_y)
  stopifnot_(
    !paired || n_x == n_y,
    "The number of observations must be the same in {.arg x} and {.arg y}
     for a paired test."
  )
  alph_x <- attr(data_x, "alphabet")
  alph_y <- attr(data_y, "alphabet")
  a <- length(alph_x)
  stopifnot_(
    a == length(alph_y),
    "The number of states of {.arg x} and {.arg y} must be the same."
  )
  stopifnot_(
    all(alph_x == alph_y),
    "The state labels of {.arg x} and {.arg y} must be the same
     and in the same order."
  )
  combined_data <- rbind(data_x, data_y)
  attr(combined_data, "alphabet") <- attr(data_x, "alphabet")
  attr(combined_data, "labels") <- attr(data_x, "labels")
  n_xy <- n_x + n_y
  weights_x <- x$weights
  weights_y <- y$weights
  type <- attr(x, "type")
  scaling <- attr(x, "scaling")
  params <- attr(x, "params")
  n_measures <- length(measures)
  include_centralities <- n_measures > 0L
  if (include_centralities) {
    cent_x <- centralities(x, measures = measures, ...)
    cent_y <- centralities(y, measures = measures, ...)
    cent_diffs_true <- as.matrix(cent_x[, -1L] - cent_y[, -1L])
    cent_diffs_true_abs <- abs(cent_diffs_true)
  }
  edge_diffs_true <- weights_x - weights_y
  edge_diffs_true_abs <- abs(edge_diffs_true)
  edge_names <- expand.grid(
    from = rownames(weights_x),
    to = colnames(weights_y)
  )
  edge_names <- paste0(edge_names$from, " -> ", edge_names$to)
  idx_x <- seq_len(n_x)
  idx_y <- seq(n_x + 1L, n_xy)
  combined_model <- initialize_model(
    combined_data,
    type,
    scaling,
    params,
    transitions = TRUE
  )
  combined_trans <- combined_model$trans
  edge_diffs_perm <- array(0L, dim = c(iter, a, a))
  cent_diffs_perm <- array(0L, dim = c(iter, a, n_measures))
  edge_p_values <- matrix(0L, a, a)
  cent_p_values <- matrix(0L, a, n_measures)
  for (i in seq_len(iter)) {
    if (paired) {
      # For paired data, permute within pairs
      pair_idx <- matrix(seq_len(n_xy), ncol = 2)
      permuted_pairs <- t(apply(pair_idx, 1, sample))
      perm_idx <- c(permuted_pairs)
    } else {
      # For unpaired data, perform complete randomization
      perm_idx <- sample(n_xy)
    }
    trans_perm_x <- combined_trans[perm_idx[idx_x], , , drop = FALSE]
    trans_perm_y <- combined_trans[perm_idx[idx_y], , , drop = FALSE]
    weights_perm_x <- compute_weights(trans_perm_x, type, scaling, a)
    weights_perm_y <- compute_weights(trans_perm_y, type, scaling, a)
    if (include_centralities) {
      cent_perm_x <- centralities(weights_perm_x, measures = measures, ...)
      cent_perm_y <- centralities(weights_perm_y, measures = measures, ...)
      cent_diffs_perm[i, , ] <- as.matrix(
        cent_perm_x[, -1L] - cent_perm_y[, -1L]
      )
      cent_p_values <- cent_p_values +
        1L * (abs(cent_diffs_perm[i, , ]) >= cent_diffs_true_abs)
    }
    edge_diffs_perm[i, , ] <- weights_perm_x - weights_perm_y
    edge_p_values <- edge_p_values +
      1L * (abs(edge_diffs_perm[i, , ]) >= edge_diffs_true_abs)
  }
  edge_p_values <- (edge_p_values + 1) / (iter + 1)
  edge_p_values[,] <- stats::p.adjust(
    p = edge_p_values,
    method = adjust
  )
  edge_diffs_sd <- apply(edge_diffs_perm, c(2, 3), stats::sd)
  edge_diffs_sig <- edge_diffs_true * (edge_p_values < level)
  edge_stats <- data.frame(
    edge_name = edge_names,
    diff_true = c(edge_diffs_true),
    effect_size = c(edge_diffs_true) / c(edge_diffs_sd),
    p_value = c(edge_p_values)
  )
  out <- list(
    edges = list(
      stats = edge_stats,
      diffs_true = edge_diffs_true,
      diffs_sig = edge_diffs_sig
    )
  )
  if (include_centralities) {
    cent_p_values <- (cent_p_values + 1) / (iter + 1)
    cent_p_values[,] <- stats::p.adjust(
      p = cent_p_values,
      method = adjust
    )
    cent_diffs_sd <- apply(cent_diffs_perm, c(2, 3), stats::sd)
    cent_diffs_sig <- cent_diffs_true * (cent_p_values < level)
    cent_stats <- expand.grid(state = cent_x$state, centrality = measures)
    cent_stats$diff_true <- c(cent_diffs_true)
    cent_stats$effect_size <- c(cent_diffs_true) / c(cent_diffs_sd)
    cent_stats$p_value <- c(cent_p_values)
    cent_diffs_true <- cbind(
      data.frame(state = cent_x$state),
      as.data.frame(cent_diffs_true)
    )
    cent_diffs_sig <- cbind(
      data.frame(state = cent_x$state),
      as.data.frame(cent_diffs_sig)
    )
    out$centralities <- list(
      stats = cent_stats,
      diffs_true = cent_diffs_true,
      diffs_sig = cent_diffs_sig
    )
  }
  structure(
    out,
    labels = x$labels,
    colors = attr(x$data, "colors"),
    class = "tna_permutation"
  )
}

permutation_test_patterns <- function(x, len, iter, adjust) {
  stopifnot_(
    length(attr(x, "scaling")) == 0L || attr(x, "groupwise"),
    "Permutation test is not supported for
     grouped models with globally scaled edge weights."
  )
  check_values(iter, strict = TRUE)
  data <- combine_data(x)
  group <- data$.group
  data$.group <- NULL
  m <- as.matrix(data)
  n <- nrow(m)
  k <- length(len)
  labels <- x[[1L]]$labels
  pattern_matrices <- extract_patterns(m, len, labels)
  patterns <- factorize_patterns(pattern_matrices, group)
  stat_true <- lapply(patterns, pattern_statistic)
  stat_perm <- vector(mode = "list", length = k)
  stat_p_value <- vector(mode = "list", length = k)
  for (j in seq_len(k)) {
    u <- length(pattern_matrices[[j]]$unique)
    stat_perm[[j]] <- matrix(0, u, iter)
    stat_p_value[[j]] <- numeric(u)
  }
  for (i in seq_len(iter)) {
    perm_idx <- sample(n)
    perm_group <- group[perm_idx]
    perm_patterns <- factorize_patterns(pattern_matrices, perm_group)
    for (j in seq_len(k)) {
      stat_perm[[j]][, i] <- pattern_statistic(perm_patterns[[j]])
      stat_p_value[[j]] <- stat_p_value[[j]] + 1L *
        (stat_perm[[j]][, i] >= stat_true[[j]])
    }
  }
  out <- vector(mode = "list", length = k)
  for (j in seq_len(k)) {
    stat_perm_mean <- apply(stat_perm[[j]], 1, mean)
    stat_perm_sd <- apply(stat_perm[[j]], 1, stats::sd)
    out[[j]] <- data.frame(
      effect_size = (stat_true[[j]] - stat_perm_mean) / stat_perm_sd,
      p_value = stats::p.adjust(
        (stat_p_value[[j]] + 1) / (iter + 1),
        method = adjust
      )
    )
  }
  do.call("rbind", out)
}

pattern_statistic <- function(x) {
  n <- sum(x)
  nr <- nrow(x)
  nc <- ncol(x)
  rs <- .rowSums(x, m = nr, n = nc)
  cs <- .colSums(x, m = nr, n = nc)
  expected <- outer(rs, cs) / n
  sqrt(.rowSums((x - expected)^2, m = nr, n = nc))
}
