#' Network Utility Functions for Edge-List Manipulation
#'
#' A small set of helpers to inspect and edit network edge lists produced by
#' \code{buildKeggNetwork()}, or related functions. These utilities make it easier to:
#' \itemize{
#'   \item check whether user-supplied nodes are present in a network,
#'   \item merge equivalent nodes under a single identifier,
#'   \item remove selected nodes (and all their incident edges),
#'   \item strip KEGG drug nodes (\code{"dr:"}) from a network.
#' }
#'
#' Each function has its own documentation and examples:
#' \itemize{
#'   \item \code{\link{findMappedNodes}} — test which nodes are mapped or unmapped.
#'   \item \code{\link{replaceNode}} — replace specified nodes with a new ID.
#'   \item \code{\link{removeNetworkNodes}} — remove specified nodes and their edges.
#'   \item \code{\link{removeDrugs}} — remove all KEGG drug nodes (\code{"dr:"}).
#' }
#'
#' @return Internal utility functions used by MetaPathNet.
#'
#' @seealso
#' \code{\link{buildKeggNetwork}}, \code{\link{buildEgoNetwork}}
#'
#' @keywords internal
#' @importFrom stats na.omit
#'
#' @name network_utils
NULL


#' Map User-Supplied Nodes to a Network
#'
#' Check which nodes from a user-supplied vector are present in a given
#' network edge list and which are absent.
#'
#' @param nodes Character vector of node IDs (e.g. KEGG compounds, KO IDs,
#'   reaction IDs) to check.
#' @param network_table Matrix or data frame (2 or 3 columns) representing a
#'   network, typically generated by \code{\link{buildKeggNetwork}} or related
#'   functions.
#'
#' @return A list with two components:
#' \describe{
#'   \item{mapped_nodes}{Character vector of nodes found in the network.}
#'   \item{unmapped_nodes}{Character vector of nodes not found in the network.}
#' }
#'
#' @details
#' This function is a small utility to verify that user-specified compounds,
#' genes/KOs, or reactions are present in a network before running downstream
#' analyses (e.g. distance calculations, shortest paths, ego networks).
#'
#' @examples
#' ## Example — Check presence of Trp-pathway nodes in a MetaPathNet example network
#'
#' ## Load the precomputed example network; see buildKeggNetwork() for its construction
#' data(MetaPathNet_example_network)
#'
#' ## Define a small set of candidate nodes (compounds + KO + fake ID)
#' candidate_nodes <- c(
#'   "C00078",       # tryptophan      (will be normalised to "cpd:C00078")
#'   "cpd:C00328",   # L-kynurenine
#'   "K00463",       # tryptophan synthase beta chain
#'   "NotID"         # fake node, should be unmapped
#' )
#'
#' ## Check which of these nodes are present in the network
#' res <- findMappedNodes(
#'   nodes         = candidate_nodes,
#'   network_table = MetaPathNet_example_network
#' )
#'
#' res$mapped_nodes    # present in MetaPathNet_example_network
#' res$unmapped_nodes  # absent from MetaPathNet_example_network
#'
#' @export
findMappedNodes <- function(nodes, network_table) {
  #==============================================================
  # Step 0 — Normalise user-supplied node IDs
  #==============================================================
  # Coerce to character and drop NAs
  nodes <- as.character(nodes)
  nodes <- nodes[!is.na(nodes)]

  # If user supplies plain Cxxxxx, convert to cpd:Cxxxxx
  # (network matrices store compounds as "cpd:Cxxxxx")
  nodes <- ifelse(
    grepl("^C\\d{5}$", nodes, ignore.case = TRUE),
    paste0("cpd:", toupper(nodes)),
    nodes
  )

  #==============================================================
  # Step 1 — Collect all unique nodes from the network
  #==============================================================
  # Works for 2- or 3-column matrices / data.frames
  all_nodes <- unique(as.vector(as.matrix(network_table)))

  #==============================================================
  # Step 2 — Identify mapped vs unmapped nodes
  #==============================================================
  mapped_nodes   <- intersect(nodes, all_nodes)
  unmapped_nodes <- setdiff(nodes, all_nodes)

  #==============================================================
  # Step 3 — Return result as a list
  #==============================================================
  list(
    mapped_nodes   = mapped_nodes,
    unmapped_nodes = unmapped_nodes
  )
}

#' Replace Nodes in a Network Table
#'
#' Replace one or more node IDs in a network matrix by a single new ID.
#' This is typically used to cluster equivalent metabolites (e.g. isomers)
#' under a common identifier before downstream analysis.
#'
#' @param nodes_to_replace Character vector of node IDs to be replaced. Compound IDs can
#'   be given as \code{"Cxxxxx"} or \code{"cpd:Cxxxxx"}; both are accepted.
#' @param replacement_node Character string giving the new node ID to use as replacement
#'   (same flexibility as \code{nodes_to_replace} for compound IDs).
#' @param network_table A 2- or 3-column matrix representing the network
#'   (e.g. as returned by \code{buildKeggNetwork()}).
#'
#' @return
#' A network matrix of the same structure as \code{network_table}, with all
#' occurrences of \code{nodes_to_replace} replaced by \code{replacement_node}. Duplicate edges
#' created by the replacement are removed.
#'
#' @details
#' All matches are performed on KEGG-style node labels. For KEGG compounds,
#' the function internally normalises \code{"Cxxxxx"} and \code{"cpd:Cxxxxx"}
#' to a consistent \code{"cpd:Cxxxxx"} form before replacement.
#'
#' @examples
#' ## 1) Load the precomputed example network; see buildKeggNetwork() for its construction
#' data(MetaPathNet_example_network)
#' net_hsa <- MetaPathNet_example_network
#'
#' ## 2) (R)- and (S)-methylmalonyl-CoA are stereoisomers and can be merged
#' ##    into a single node
#' nodes_to_merge   <- c("cpd:C01213", "cpd:C00683")
#' replacement_node <- "cpd:C00683"
#'
#' ## 3) Replace the selected nodes with one common identifier
#' net_hsa_clustered <- replaceNode(
#'   nodes_to_replace = nodes_to_merge,
#'   replacement_node = replacement_node,
#'   network_table    = net_hsa
#' )
#'
#' ## 4) Check that the replacement was applied in the updated network
#' findMappedNodes(
#'   nodes         = c("cpd:C01213", "cpd:C00683"),
#'   network_table = net_hsa_clustered
#' )
#'
#' @seealso \code{\link{findMappedNodes}}
#' @export
replaceNode <- function(nodes_to_replace, replacement_node, network_table) {
  #==============================================================
  # Step 0 — Basic checks and normalisation
  #==============================================================
  # Ensure at least two columns (source, target)
  stopifnot(ncol(network_table) >= 2)

  # Work internally with a character matrix
  net <- as.matrix(network_table)
  mode(net) <- "character"

  # Normalise compound-like IDs to "cpd:Cxxxxx" form
  normalize_cpd_id <- function(x) {
    x <- gsub(" ", "", as.character(x))

    vapply(x, function(id) {
      if (grepl("^cpd:C\\d{5}$", id, ignore.case = TRUE)) {
        paste0("cpd:", toupper(sub("^cpd:", "", id, ignore.case = TRUE)))
      } else if (grepl("^C\\d{5}$", id, ignore.case = TRUE)) {
        paste0("cpd:", toupper(id))
      } else {
        id
      }
    }, character(1))
  }

  #==============================================================
  # Step 1 — Normalise replacement IDs
  #==============================================================
  node1_norm <- normalize_cpd_id(nodes_to_replace)
  node2_norm <- normalize_cpd_id(replacement_node)[1L]

  #==============================================================
  # Step 2 — Apply node replacement
  #==============================================================
  for (old_id in node1_norm) {
    net[net == old_id] <- node2_norm
  }

  #==============================================================
  # Step 3 — Remove duplicate edges
  #==============================================================
  net <- unique(net)

  return(as.matrix(net))
}

#' Remove Specified Nodes from a Network Matrix
#'
#' Removes one or more nodes (compounds, KOs, reactions, etc.) from a network
#' edge list. All edges involving any of the specified nodes are removed.
#'
#' @param nodes_to_remove Character vector of node IDs to remove from the network
#'   (e.g. KEGG compound IDs, KO IDs). For compounds, both \code{"Cxxxxx"}
#'   and \code{"cpd:Cxxxxx"} formats are accepted; internally they are
#'   normalised to \code{"cpd:Cxxxxx"}.
#' @param network_table A matrix (typically 2 or 3 columns) representing the
#'   network edge list, as generated by \code{buildKeggNetwork()} or related functions.
#'
#' @return
#' A matrix of the same format as \code{network_table}, with all edges
#' involving the specified nodes removed and duplicate edges dropped.
#'
#' @details
#' All occurrences of the supplied node IDs are set to \code{NA}, and all
#' rows containing \code{NA} are then removed. This is useful to prune
#' unwanted nodes before downstream analyses such as distance or shortest-path
#' calculations.
#'
#' @examples
#' ## Example — Remove a selected node from a human integrated network
#'
#' ## 1) Load the precomputed example network; see buildKeggNetwork() for its construction
#' data(MetaPathNet_example_network)
#' net_hsa <- MetaPathNet_example_network
#'
#' ## 2) Define node to remove
#' nodes_to_remove <- "cpd:C01213"   # (R)-Methylmalonyl-CoA
#'
#' ## 3) Remove all edges involving the selected node
#' net_hsa_filtered <- removeNetworkNodes(
#'   nodes_to_remove = nodes_to_remove,
#'   network_table   = net_hsa
#' )
#'
#' ## 4) Check that the node is no longer present
#' findMappedNodes(
#'   nodes         = nodes_to_remove,
#'   network_table = net_hsa_filtered
#' )$mapped_nodes
#'
#' ## 5) Compare network size before and after node removal
#' dim(net_hsa)           # Original network dimensions
#' dim(net_hsa_filtered)  # Network dimensions after node removal
#' nrow(net_hsa) - nrow(net_hsa_filtered)  # Number of removed edges
#'
#' @importFrom stats na.omit
#' @export
removeNetworkNodes <- function(nodes_to_remove, network_table) {
  #==============================================================
  # Step 0 — Basic checks and normalisation
  #==============================================================
  stopifnot(ncol(network_table) >= 2)

  network_table <- as.matrix(network_table)
  storage.mode(network_table) <- "character"

  nodes_to_remove <- unique(as.character(nodes_to_remove))

  #==============================================================
  # Step 1 — Normalise compound-like IDs to cpd:Cxxxxx
  #==============================================================
  is_compound <- grepl("^(cpd:)?[Cc][0-9]{5}$", nodes_to_remove)

  if (any(is_compound)) {
    core_ids <- toupper(gsub("^(cpd:)?", "", nodes_to_remove[is_compound]))
    nodes_to_remove[is_compound] <- paste0("cpd:", core_ids)
  }

  #==============================================================
  # Step 2 — Remove all edges involving selected nodes
  #==============================================================
  for (id in nodes_to_remove) {
    network_table[network_table == id] <- NA
  }

  network_table <- stats::na.omit(network_table)
  network_table <- unique(network_table)

  return(as.matrix(network_table))
}

#' Remove Drug Nodes from a Network Matrix
#'
#' Removes all edges from a network where either endpoint is annotated as a
#' KEGG drug (prefix \code{"dr:"}).
#'
#' @param network_table A two- or three-column matrix representing the network
#'   (edge list), typically as returned by \code{buildKeggNetwork()} or related
#'   functions.
#'
#' @return A matrix of the same format as the input, with all drug nodes
#'   (prefix \code{"dr:"}) and their incident edges removed.
#'
#' @details
#' The function scans the first two columns (source and target) for node IDs
#' containing the \code{"dr:"} prefix. All rows involving such nodes are
#' removed. If no drug nodes are found, the original matrix is returned and
#' a message is printed.
#'
#' @examples
#' ## 1) Prepare an example network containing KEGG drug nodes
#' data(MetaPathNet_example_network)
#'
#' net_hsa <- rbind(
#'   MetaPathNet_example_network,
#'   c("dr:D00001", "K00463", "drug_example"),
#'   c("K00463", "dr:D00002", "drug_example")
#' )
#'
#' ## 2) Remove all KEGG drug nodes (dr:...) from the network
#' net_hsa_nodrug <- removeDrugs(net_hsa)
#'
#' ## 3) Compare dimensions before and after drug removal
#' dim(net_hsa)         # Original network dimensions
#' dim(net_hsa_nodrug)  # Network dimensions after drug removal
#' nrow(net_hsa) - nrow(net_hsa_nodrug)  # Number of drug-related edges removed
#'
#' @export
removeDrugs <- function(network_table) {
  #==============================================================
  # Step 1 — Basic input checks
  #==============================================================
  stopifnot(ncol(network_table) >= 2)

  #==============================================================
  # Step 2 — Detect and remove drug nodes (dr:)
  #==============================================================
  # Indices of edges where source or target is a KEGG drug
  ind <- unique(c(
    grep("dr:", network_table[, 1]),
    grep("dr:", network_table[, 2])
  ))

  if (length(ind) > 0) {
    # Collect unique drug node IDs being removed
    drug_nodes <- unique(c(
      network_table[ind, 1][grepl("dr:", network_table[ind, 1])],
      network_table[ind, 2][grepl("dr:", network_table[ind, 2])]
    ))

    # Remove corresponding edges
    network_table <- network_table[-ind, , drop = FALSE]
    message("Drug nodes removed: ", paste(drug_nodes, collapse = ", "))

    return(as.matrix(network_table))
  }

  message("No drug nodes found in the network.")
  return(as.matrix(network_table))
}

#' Merge Multiple MetaPathNet-Style Networks
#'
#' Merges two or more MetaPathNet-style edge lists into a single network.
#' All inputs must be 3-column matrices or data.frames corresponding to
#' \code{source}, \code{target}, and \code{interaction_type}.
#'
#' @param ... Two or more MetaPathNet-style network tables.
#'
#' @return
#' A 3-column matrix containing the merged MetaPathNet-style edge list with
#' duplicated rows removed.
#'
#' @details
#' \itemize{
#'   \item All inputs must be MetaPathNet-style edge lists with at least
#'         3 columns corresponding to \code{source}, \code{target}, and
#'         \code{interaction_type}.
#'   \item Only the first 3 columns of each input are used.
#'   \item Duplicated rows across input networks are removed after merging.
#' }
#'
#' @seealso
#' \code{\link{buildKeggNetwork}}, \code{\link{buildCrossSpeciesNetwork}}
#'
#' @examples
#' ## Example 1 — Merge two example network subsets
#'
#' ## Load the precomputed example network; see buildKeggNetwork() for its construction
#' data(MetaPathNet_example_network)
#'
#' ## Create two small example network subsets
#' example_network_1 <- MetaPathNet_example_network[seq_len(20), , drop = FALSE]
#' example_network_2 <- MetaPathNet_example_network[21:40, , drop = FALSE]
#'
#' ## Merge both subsets into one edge list
#' merged_network <- combineNetworks(example_network_1, example_network_2)
#'
#' head(merged_network)
#'
#' @export
combineNetworks <- function(...) {
  #==============================================================
  # Step 1 - Collect inputs
  #==============================================================
  networks <- list(...)

  if (length(networks) < 2) {
    stop("combineNetworks() requires at least two network tables.")
  }

  #==============================================================
  # Step 2 - Check MetaPathNet-style edge list format
  #==============================================================
  check_network_table <- function(x, index) {
    if (!is.matrix(x) && !is.data.frame(x)) {
      stop(
        "Input ", index,
        " must be a matrix or data.frame in MetaPathNet-style edge list format ",
        "(3 columns: source, target, interaction_type)."
      )
    }

    x <- as.matrix(x)

    if (ncol(x) < 3) {
      stop(
        "Input ", index,
        " must contain at least 3 columns: source, target, interaction_type."
      )
    }

    x <- x[, seq_len(3), drop = FALSE]
    storage.mode(x) <- "character"
    colnames(x) <- c("source", "target", "interaction_type")

    x
  }

  networks_checked <- lapply(seq_along(networks), function(i) {
    check_network_table(networks[[i]], i)
  })

  #==============================================================
  # Step 3 - Merge networks
  #==============================================================
  merged_network <- unique(do.call(rbind, networks_checked))

  rownames(merged_network) <- NULL
  merged_network
}
