#' Identify top marker genes and summarize expression statistics
#'
#' @description
#' `CellWindX_TopMarkersStats()` identifies the top marker genes for each
#' annotated cell group in a Seurat object and summarizes their expression
#' percentage and average expression. The output is designed to be directly used
#' by downstream CellWindX visualization functions, including
#' `CellWindX_MarkerHeatmap()` and `CellWindX_GeneRadar()`.
#'
#' @details
#' This function first runs [Seurat::FindAllMarkers()] using the cell-group
#' annotation specified by `group.by`. For each group, the top `N` marker genes
#' are selected according to adjusted P value, log-fold change, and detection
#' percentage.
#'
#' For each selected marker gene, the function then calculates:
#'
#' \itemize{
#'   \item `n_cells`: number of cells in the target group.
#'   \item `n_expr_cells`: number of cells with expression greater than
#'   `expression.threshold`.
#'   \item `pct_expr`: percentage of expressing cells.
#'   \item `avg_expr`: average expression across all cells in the group.
#'   \item `avg_expr_positive`: average expression among expressing cells only.
#' }
#'
#' The function returns both source-cluster-specific statistics and expression
#' summaries across all annotated groups. This makes it suitable for marker
#' inspection, heatmap visualization, dot-plot-style summaries, and radar plots.
#'
#' The function is compatible with Seurat v4 and v5 by attempting to retrieve
#' assay data through `layer` first and falling back to `slot` when needed.
#'
#' @param object A Seurat object.
#' @param group.by Character string. Metadata column used as cell-group
#'   annotation for marker detection. Default is `"seurat_annotations"`.
#' @param assay Character string or `NULL`. Assay used for marker detection and
#'   expression summarization. If `NULL`, [Seurat::DefaultAssay()] is used.
#'   Default is `NULL`.
#' @param slot Character string. Assay slot or layer used to calculate expression
#'   statistics. Common choices are `"data"` for normalized expression and
#'   `"counts"` for raw counts. Default is `"data"`.
#' @param top_n Integer. Number of top marker genes selected for each cell group.
#'   Default is `5`.
#' @param only.pos Logical. Whether to return only positive markers in
#'   [Seurat::FindAllMarkers()]. Default is `TRUE`.
#' @param min.pct Numeric. Minimum fraction of cells expressing a gene in either
#'   of the two groups tested by [Seurat::FindAllMarkers()]. Default is `0.25`.
#' @param logfc.threshold Numeric. Log-fold-change threshold used by
#'   [Seurat::FindAllMarkers()]. Default is `0.25`.
#' @param test.use Character string. Statistical test used by
#'   [Seurat::FindAllMarkers()]. Default is `"wilcox"`.
#' @param min.diff.pct Numeric. Minimum difference in detection percentage
#'   between the two groups compared by [Seurat::FindAllMarkers()]. Default is
#'   `-Inf`.
#' @param expression.threshold Numeric. Threshold used to define whether a gene
#'   is expressed in a cell when calculating `n_expr_cells` and `pct_expr`.
#'   Default is `0`.
#' @param exclude.mt Logical. Whether to remove mitochondrial genes from selected
#'   top markers. Human mitochondrial genes beginning with `"MT-"` and mouse
#'   mitochondrial genes beginning with `"mt-"` are removed. Default is `TRUE`.
#' @param exclude.ribo Logical. Whether to remove ribosomal protein genes
#'   beginning with `"RPS"`, `"RPL"`, `"Rps"`, or `"Rpl"`. Default is `FALSE`.
#' @param output_dir Character string or `NULL`. Optional directory for saving
#'   result tables as CSV files. If `NULL`, no files are written. Default is
#'   `NULL`.
#' @param file_prefix Character string. Prefix used for output CSV files when
#'   `output_dir` is not `NULL`. Default is `"CellWindX_top_markers"`.
#' @param seed Integer. Random seed set before marker detection. Default is
#'   `123`.
#' @param verbose Logical. Whether to print progress messages. Default is
#'   `TRUE`.
#'
#' @return A list with class `"CellWindX_TopMarkersStats"` containing:
#' \describe{
#'   \item{markers_all}{Complete marker table returned by
#'   [Seurat::FindAllMarkers()].}
#'   \item{top_markers}{Top marker genes selected for each source cell group.}
#'   \item{top_marker_stats}{Expression statistics of each selected marker gene
#'   within its source cell group.}
#'   \item{marker_expr_by_group}{Expression statistics of selected marker genes
#'   across all target cell groups. This table is used by downstream CellWindX
#'   heatmap and radar functions.}
#'   \item{top_gene_table}{A compact table listing top marker genes for each
#'   cell group.}
#'   \item{parameters}{A list of parameters used in the analysis.}
#' }
#'
#' @author Xiaofeng Yang, Chongqing Medical University
#'
#' @examples
#' counts <- matrix(
#'   c(
#'     25, 26, 24, 27, 25, 26, 24, 28, 25, 27,
#'      1,  2,  1,  1,  2,  1,  1,  2,  1,  1,
#'      1,  1,  2,  1,  1,  2,  1,  1,  2,  1,
#'
#'      1,  1,  2,  1,  1,  2,  1,  1,  2,  1,
#'     25, 27, 26, 24, 28, 25, 27, 26, 24, 28,
#'      1,  2,  1,  1,  2,  1,  1,  2,  1,  1,
#'
#'      1,  2,  1,  1,  2,  1,  1,  2,  1,  1,
#'      1,  1,  2,  1,  1,  2,  1,  1,  2,  1,
#'     25, 26, 28, 24, 27, 25, 26, 28, 24, 27,
#'
#'      5,  6,  5,  6,  5,  6,  5,  6,  5,  6,
#'      5,  6,  5,  6,  5,  6,  5,  6,  5,  6,
#'      5,  6,  5,  6,  5,  6,  5,  6,  5,  6
#'   ),
#'   nrow = 4,
#'   byrow = TRUE
#' )
#'
#' rownames(counts) <- c("CD3D", "MS4A1", "LYZ", "ACTB")
#' colnames(counts) <- paste0("Cell", seq_len(ncol(counts)))
#'
#' counts <- Matrix::Matrix(counts, sparse = TRUE)
#'
#' object <- Seurat::CreateSeuratObject(counts = counts)
#' object$cell_type <- rep(c("T cell", "B cell", "Myeloid"), each = 10)
#' object <- Seurat::NormalizeData(object, verbose = FALSE)
#'
#' res_marker <- CellWindX_TopMarkersStats(
#'   object = object,
#'   group.by = "cell_type",
#'   assay = "RNA",
#'   slot = "data",
#'   top_n = 1,
#'   only.pos = TRUE,
#'   min.pct = 0.10,
#'   logfc.threshold = 0,
#'   test.use = "t",
#'   verbose = FALSE
#' )
#'
#' head(res_marker$top_markers)
#' head(res_marker$top_marker_stats)
#' head(res_marker$marker_expr_by_group)
#' res_marker$top_gene_table
#'
#' @export
CellWindX_TopMarkersStats <- function(
    object,
    group.by = "seurat_annotations",
    assay = NULL,
    slot = "data",
    top_n = 5,
    only.pos = TRUE,
    min.pct = 0.25,
    logfc.threshold = 0.25,
    test.use = "wilcox",
    min.diff.pct = -Inf,
    expression.threshold = 0,
    exclude.mt = TRUE,
    exclude.ribo = FALSE,
    output_dir = NULL,
    file_prefix = "CellWindX_top_markers",
    seed = 123,
    verbose = TRUE
) {

  # -----------------------------
  # 1. Package check
  # -----------------------------
  if (!requireNamespace("Seurat", quietly = TRUE)) {
    stop("Package 'Seurat' is required.")
  }
  if (!requireNamespace("Matrix", quietly = TRUE)) {
    stop("Package 'Matrix' is required.")
  }

  # -----------------------------
  # 2. Basic checks
  # -----------------------------
  if (!inherits(object, "Seurat")) {
    stop("'object' must be a Seurat object.")
  }

  if (!group.by %in% colnames(object@meta.data)) {
    stop(
      paste0(
        "The metadata column '", group.by,
        "' was not found in object@meta.data."
      )
    )
  }

  if (is.null(assay)) {
    assay <- Seurat::DefaultAssay(object)
  }

  if (!assay %in% names(object@assays)) {
    stop(
      paste0(
        "Assay '", assay,
        "' was not found in the Seurat object."
      )
    )
  }

  top_n <- as.integer(top_n)

  if (top_n < 1) {
    stop("'top_n' must be >= 1.")
  }

  # -----------------------------
  # 3. Compatible GetAssayData
  # -----------------------------
  get_assay_matrix <- function(object, assay, slot) {

    mat <- tryCatch(
      {
        Seurat::GetAssayData(
          object = object,
          assay = assay,
          layer = slot
        )
      },
      error = function(e1) {
        Seurat::GetAssayData(
          object = object,
          assay = assay,
          slot = slot
        )
      }
    )

    return(mat)
  }

  # -----------------------------
  # 4. Prepare identities
  # -----------------------------
  set.seed(seed)

  old_ident <- Seurat::Idents(object)

  object@meta.data[[group.by]] <- as.factor(object@meta.data[[group.by]])
  Seurat::Idents(object) <- object@meta.data[[group.by]]

  group_levels <- levels(object@meta.data[[group.by]])

  if (verbose) {
    message("CellWindX: group.by = ", group.by)
    message("CellWindX: assay = ", assay)
    message("CellWindX: slot/layer = ", slot)
    message("CellWindX: number of groups = ", length(group_levels))
  }

  # -----------------------------
  # 5. Run FindAllMarkers
  # -----------------------------
  markers_all <- Seurat::FindAllMarkers(
    object = object,
    assay = assay,
    only.pos = only.pos,
    min.pct = min.pct,
    logfc.threshold = logfc.threshold,
    test.use = test.use,
    min.diff.pct = min.diff.pct,
    verbose = verbose
  )

  Seurat::Idents(object) <- old_ident

  if (nrow(markers_all) == 0) {
    stop("No marker genes were identified. Try lowering min.pct or logfc.threshold.")
  }

  # -----------------------------
  # 6. Standardize marker columns
  # -----------------------------
  if (!"gene" %in% colnames(markers_all)) {
    markers_all$gene <- rownames(markers_all)
  }

  fc_col <- NULL

  if ("avg_log2FC" %in% colnames(markers_all)) {
    fc_col <- "avg_log2FC"
  } else if ("avg_logFC" %in% colnames(markers_all)) {
    fc_col <- "avg_logFC"
  } else {
    stop("Cannot find avg_log2FC or avg_logFC column in FindAllMarkers output.")
  }

  if (!"cluster" %in% colnames(markers_all)) {
    stop("Cannot find 'cluster' column in FindAllMarkers output.")
  }

  markers_all$cluster <- as.character(markers_all$cluster)
  markers_all$gene <- as.character(markers_all$gene)

  # -----------------------------
  # 7. Optional gene filtering
  # -----------------------------
  markers_filtered <- markers_all

  if (exclude.mt) {
    markers_filtered <- markers_filtered[
      !grepl("^MT-", markers_filtered$gene) &
        !grepl("^mt-", markers_filtered$gene),
      ,
      drop = FALSE
    ]
  }

  if (exclude.ribo) {
    markers_filtered <- markers_filtered[
      !grepl("^RPS|^RPL|^Rps|^Rpl", markers_filtered$gene),
      ,
      drop = FALSE
    ]
  }

  if (nrow(markers_filtered) == 0) {
    stop("All markers were removed after filtering.")
  }

  # -----------------------------
  # 8. Select Top N markers per group
  # -----------------------------
  marker_split <- split(markers_filtered, markers_filtered$cluster)

  top_marker_list <- lapply(marker_split, function(df) {

    df <- df[order(
      df$p_val_adj,
      -df[[fc_col]],
      -df$pct.1
    ), , drop = FALSE]

    df <- df[seq_len(min(top_n, nrow(df))), , drop = FALSE]

    df$rank_in_cluster <- seq_len(nrow(df))

    return(df)
  })

  top_markers <- do.call(rbind, top_marker_list)
  rownames(top_markers) <- NULL

  useful_cols <- c(
    "cluster",
    "rank_in_cluster",
    "gene",
    "p_val",
    "p_val_adj",
    fc_col,
    "pct.1",
    "pct.2"
  )

  useful_cols <- useful_cols[useful_cols %in% colnames(top_markers)]
  top_markers <- top_markers[, c(useful_cols, setdiff(colnames(top_markers), useful_cols)), drop = FALSE]

  # -----------------------------
  # 9. Extract expression matrix
  # -----------------------------
  expr_mat <- get_assay_matrix(
    object = object,
    assay = assay,
    slot = slot
  )

  if (nrow(expr_mat) == 0 || ncol(expr_mat) == 0) {
    stop(
      paste0(
        "The selected assay slot/layer '", slot,
        "' is empty. Please run NormalizeData(object) first or use slot = 'counts'."
      )
    )
  }

  if (is.null(rownames(expr_mat)) || is.null(colnames(expr_mat))) {
    stop("Expression matrix must have rownames as genes and colnames as cells.")
  }

  # Keep only cells present in both meta.data and expression matrix
  common_cells <- intersect(rownames(object@meta.data), colnames(expr_mat))

  if (length(common_cells) == 0) {
    stop("No overlapping cell names between object@meta.data and expression matrix.")
  }

  meta_df <- object@meta.data[common_cells, , drop = FALSE]
  expr_mat <- expr_mat[, common_cells, drop = FALSE]

  meta_df[[group.by]] <- as.factor(meta_df[[group.by]])
  group_levels <- levels(meta_df[[group.by]])

  # Keep only top markers present in expression matrix
  top_markers$gene_in_matrix <- top_markers$gene %in% rownames(expr_mat)

  if (any(!top_markers$gene_in_matrix)) {
    warning(
      "Some selected marker genes were not found in the expression matrix and will be removed: ",
      paste(top_markers$gene[!top_markers$gene_in_matrix], collapse = ", ")
    )
  }

  top_markers <- top_markers[top_markers$gene_in_matrix, , drop = FALSE]

  if (nrow(top_markers) == 0) {
    stop("No selected top marker genes were found in the expression matrix.")
  }

  # -----------------------------
  # 10. Robust expression statistics
  # -----------------------------
  calc_gene_stats <- function(gene_vec, target_cells, expr_mat, threshold = 0) {

    gene_vec <- unique(as.character(gene_vec))
    target_cells <- unique(as.character(target_cells))

    gene_vec_valid <- intersect(gene_vec, rownames(expr_mat))
    target_cells_valid <- intersect(target_cells, colnames(expr_mat))

    if (length(gene_vec_valid) == 0 || length(target_cells_valid) == 0) {
      return(
        data.frame(
          gene = gene_vec,
          n_cells = length(target_cells_valid),
          n_expr_cells = NA_real_,
          pct_expr = NA_real_,
          avg_expr = NA_real_,
          avg_expr_positive = NA_real_,
          stringsAsFactors = FALSE
        )
      )
    }

    sub_mat <- expr_mat[gene_vec_valid, target_cells_valid, drop = FALSE]

    n_cells <- length(target_cells_valid)

    detected_mat <- sub_mat > threshold

    n_expr_cells <- Matrix::rowSums(detected_mat)
    pct_expr <- n_expr_cells / n_cells * 100
    avg_expr <- Matrix::rowMeans(sub_mat)

    avg_expr_positive <- rep(0, length(gene_vec_valid))
    names(avg_expr_positive) <- gene_vec_valid

    for (i in seq_along(gene_vec_valid)) {
      x <- as.numeric(sub_mat[i, ])
      x_pos <- x[x > threshold]

      if (length(x_pos) == 0) {
        avg_expr_positive[i] <- 0
      } else {
        avg_expr_positive[i] <- mean(x_pos)
      }
    }

    out <- data.frame(
      gene = gene_vec_valid,
      n_cells = n_cells,
      n_expr_cells = as.numeric(n_expr_cells),
      pct_expr = as.numeric(pct_expr),
      avg_expr = as.numeric(avg_expr),
      avg_expr_positive = as.numeric(avg_expr_positive),
      stringsAsFactors = FALSE
    )

    return(out)
  }

  # -----------------------------
  # 11. Marker stats in source cluster
  # -----------------------------
  top_marker_stats_list <- lapply(seq_len(nrow(top_markers)), function(i) {

    source_cluster <- as.character(top_markers$cluster[i])
    gene_i <- as.character(top_markers$gene[i])

    cells_i <- rownames(meta_df)[as.character(meta_df[[group.by]]) == source_cluster]

    stat_i <- calc_gene_stats(
      gene_vec = gene_i,
      target_cells = cells_i,
      expr_mat = expr_mat,
      threshold = expression.threshold
    )

    stat_i$source_cluster <- source_cluster
    stat_i$rank_in_cluster <- top_markers$rank_in_cluster[i]
    stat_i[[fc_col]] <- top_markers[[fc_col]][i]

    if ("p_val" %in% colnames(top_markers)) {
      stat_i$p_val <- top_markers$p_val[i]
    }

    if ("p_val_adj" %in% colnames(top_markers)) {
      stat_i$p_val_adj <- top_markers$p_val_adj[i]
    }

    if ("pct.1" %in% colnames(top_markers)) {
      stat_i$FindAllMarkers_pct1 <- top_markers$pct.1[i]
    }

    if ("pct.2" %in% colnames(top_markers)) {
      stat_i$FindAllMarkers_pct2 <- top_markers$pct.2[i]
    }

    return(stat_i)
  })

  top_marker_stats <- do.call(rbind, top_marker_stats_list)
  rownames(top_marker_stats) <- NULL

  top_marker_stats <- top_marker_stats[, c(
    "source_cluster",
    "rank_in_cluster",
    "gene",
    "n_cells",
    "n_expr_cells",
    "pct_expr",
    "avg_expr",
    "avg_expr_positive",
    fc_col,
    intersect(
      c("p_val", "p_val_adj", "FindAllMarkers_pct1", "FindAllMarkers_pct2"),
      colnames(top_marker_stats)
    )
  ), drop = FALSE]

  # -----------------------------
  # 12. Marker expression across all groups
  # -----------------------------
  all_group_stats_list <- list()
  idx <- 1

  for (source_cluster in unique(as.character(top_markers$cluster))) {

    genes_i <- unique(top_markers$gene[top_markers$cluster == source_cluster])

    for (target_cluster in group_levels) {

      cells_j <- rownames(meta_df)[as.character(meta_df[[group.by]]) == target_cluster]

      stat_j <- calc_gene_stats(
        gene_vec = genes_i,
        target_cells = cells_j,
        expr_mat = expr_mat,
        threshold = expression.threshold
      )

      stat_j$source_cluster <- source_cluster
      stat_j$target_cluster <- target_cluster

      all_group_stats_list[[idx]] <- stat_j
      idx <- idx + 1
    }
  }

  marker_expr_by_group <- do.call(rbind, all_group_stats_list)
  rownames(marker_expr_by_group) <- NULL

  marker_expr_by_group <- marker_expr_by_group[, c(
    "source_cluster",
    "target_cluster",
    "gene",
    "n_cells",
    "n_expr_cells",
    "pct_expr",
    "avg_expr",
    "avg_expr_positive"
  ), drop = FALSE]

  # -----------------------------
  # 13. Top gene list table
  # -----------------------------
  top_gene_list <- split(top_markers$gene, top_markers$cluster)

  top_gene_table <- data.frame(
    cluster = names(top_gene_list),
    top_genes = sapply(top_gene_list, function(x) paste(unique(x), collapse = ", ")),
    stringsAsFactors = FALSE
  )

  # -----------------------------
  # 14. Save files
  # -----------------------------
  if (!is.null(output_dir)) {

    if (!dir.exists(output_dir)) {
      dir.create(output_dir, recursive = TRUE)
    }

    write.csv(
      markers_all,
      file = file.path(output_dir, paste0(file_prefix, "_FindAllMarkers_all.csv")),
      row.names = FALSE
    )

    write.csv(
      top_markers,
      file = file.path(output_dir, paste0(file_prefix, "_top", top_n, "_markers.csv")),
      row.names = FALSE
    )

    write.csv(
      top_marker_stats,
      file = file.path(output_dir, paste0(file_prefix, "_top", top_n, "_stats_in_source_cluster.csv")),
      row.names = FALSE
    )

    write.csv(
      marker_expr_by_group,
      file = file.path(output_dir, paste0(file_prefix, "_top", top_n, "_expression_by_all_groups.csv")),
      row.names = FALSE
    )

    write.csv(
      top_gene_table,
      file = file.path(output_dir, paste0(file_prefix, "_top", top_n, "_gene_list.csv")),
      row.names = FALSE
    )
  }

  # -----------------------------
  # 15. Return
  # -----------------------------
  result <- list(
    markers_all = markers_all,
    top_markers = top_markers,
    top_marker_stats = top_marker_stats,
    marker_expr_by_group = marker_expr_by_group,
    top_gene_table = top_gene_table,
    parameters = list(
      group.by = group.by,
      assay = assay,
      slot = slot,
      top_n = top_n,
      only.pos = only.pos,
      min.pct = min.pct,
      logfc.threshold = logfc.threshold,
      test.use = test.use,
      expression.threshold = expression.threshold,
      exclude.mt = exclude.mt,
      exclude.ribo = exclude.ribo
    )
  )

  class(result) <- c("CellWindX_TopMarkersStats", class(result))

  return(result)
}
