#' Check if given liger object if under new implementation
#' @param object A liger object
#' @return \code{TRUE} if the version of \code{object} is later than or equal to
#' 1.99.0. Otherwise \code{FALSE}. It raises an error if input object is not of
#' \linkS4class{liger} class.
#' @export
#' @examples
#' is.newLiger(pbmc) # TRUE
is.newLiger <- function(object) {
    if (!inherits(object, "liger"))
        cli::cli_abort("{.var object} is not even of {.cls liger} class.")
    v <- object@version
    v1990 <- package_version("1.99.0")
    if (v >= v1990) TRUE
    else FALSE
}

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Base Generic Methods ####
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#' @param x,object,model A \linkS4class{liger} object
#' @param dataset Name or numeric index of a dataset
#' @param value Check detail sections for requirements.
#' @param type When using \code{dataset<-} with a matrix like \code{value},
#' specify what type the matrix is. Choose from \code{"rawData"},
#' \code{"normData"} or \code{"scaleData"}.
#' @param qc Logical, whether to perform general qc on added new dataset.
#' @param check Logical, whether to perform object validity check on setting new
#' value. Users are not supposed to set \code{FALSE} here.
#' @param columns The names of available variables in \code{cellMeta} slot. When
#' \code{as.data.frame = TRUE}, please use variable names after coercion.
#' @param name The name of available variables in \code{cellMeta} slot or the
#' name of a new variable to store.
#' @param cellIdx Valid cell subscription to subset retrieved variables. Default
#' \code{NULL} uses all cells.
#' @param as.data.frame Logical, whether to apply
#' \code{\link[base]{as.data.frame}} on the subscription. Default \code{FALSE}.
#' @param slot Name of slot to retrieve matrix from. Options shown in Usage.
#' @param returnList Logical, whether to force return a list even when only one
#' dataset-specific matrix (i.e. expression matrices, H, V or U) is requested.
#' Default \code{FALSE}.
#' @param useDatasets Setter or getter method should only apply on cells in
#' specified datasets. Any valid character, numeric or logical subscriber is
#' acceptable. Default \code{NULL} works with all datasets.
#' @param funcName,arg See Command records section.
#' @param data fortify method required argument. Not used.
#' @param ... See detailed sections for explanation.
#' @return See detailed sections for explanetion.
#' @export
#' @rdname liger-class
#' @examples
#' # Methods for base generics
#' pbmcPlot
#' print(pbmcPlot)
#' dim(pbmcPlot)
#' ncol(pbmcPlot)
#' colnames(pbmcPlot)[1:5]
#' pbmcPlot[varFeatures(pbmcPlot)[1:10], 1:10]
#' names(pbmcPlot)
#' length(pbmcPlot)
#'
#' # rliger generics
#' ## Retrieving dataset(s), replacement methods available
#' datasets(pbmcPlot)
#' dataset(pbmcPlot, "ctrl")
#' dataset(pbmcPlot, 2)
#'
#' ## Retrieving cell metadata, replacement methods available
#' cellMeta(pbmcPlot)
#' head(pbmcPlot[["nUMI"]])
#'
#' ## Retrieving dimemtion reduction matrix
#' head(dimRed(pbmcPlot, "UMAP"))
#'
#' ## Retrieving variable features, replacement methods available
#' varFeatures(pbmcPlot)
#'
#' ## Command record/history
#' pbmcPlot <- scaleNotCenter(pbmcPlot)
#' commands(pbmcPlot)
#' commands(pbmcPlot, funcName = "scaleNotCenter")
#'
#' # S3 methods
#' pbmcPlot2 <- pbmcPlot
#' names(pbmcPlot2) <- paste0(names(pbmcPlot), 2)
#' c(pbmcPlot, pbmcPlot2)
#'
#' library(ggplot2)
#' ggplot(pbmcPlot, aes(x = UMAP_1, y = UMAP_2)) + geom_point()
setMethod(
    f = "show",
    signature(object = "liger"),
    definition = function(object) {
        .checkObjVersion(object)
        cat("An object of class liger with", ncol(object), "cells\n")
        cat(paste0("datasets(", length(object), "): "))
        datasetInfos <- paste0(names(object), " (",
                               unlist(lapply(datasets(object), ncol)),
                               " cells)")
        cat(.collapseLongNames(datasetInfos), "\n")
        cat(paste0("cellMeta(", ncol(cellMeta(object)), "): "))
        cat(.collapseLongNames(colnames(cellMeta(object))), "\n")
        cat(paste0("varFeatures(", length(varFeatures(object)), "): "))
        cat(.collapseLongNames(varFeatures(object)), "\n")
        cat(paste0("dimReds(", length(object@dimReds), "): "))
        cat(.collapseLongNames(names(object@dimReds)), "\n")
        invisible(x = NULL)
    }
)

#' @section Dimensionality:
#' For a \code{liger} object, the column orientation is assigned for
#' cells. Due to the data structure, it is hard to define a row index for the
#' \code{liger} object, which might contain datasets that vary in number of
#' genes.
#'
#' Therefore, for \code{liger} objects, \code{dim} and \code{dimnames} returns
#' \code{NA}/\code{NULL} for rows and total cell counts/barcodes for the
#' columns.
#'
#' For direct call of \code{dimnames<-} method, \code{value} should be a list
#' with \code{NULL} as the first element and valid cell identifiers as the
#' second element. For \code{colnames<-} method, the character vector of cell
#' identifiers. \code{rownames<-} method is not applicable.
#' @section Subsetting:
#' For more detail of subsetting a \code{liger} object or a
#' \linkS4class{ligerDataset} object, please check out \code{\link{subsetLiger}}
#' and \code{\link{subsetLigerDataset}}. Here, we set the S4 method
#' "single-bracket" \code{[} as a quick wrapper to subset a \code{liger} object.
#' Note that \code{j} serves as cell subscriptor which can be any valid index
#' refering the collection of all cells (i.e. \code{rownames(cellMeta(obj))}).
#' While \code{i}, the feature subscriptor can only be character vector because
#' the features for each dataset can vary. \code{...} arugments are passed to
#' \code{subsetLiger} so that advanced options are allowed.
#' @rdname liger-class
#' @export
setMethod("dim", "liger", function(x) {
    c(NA, nrow(cellMeta(x)))
})

#' @rdname liger-class
#' @export
setMethod("dimnames", "liger", function(x) {
    list(NULL, rownames(cellMeta(x)))
})

#' @rdname liger-class
#' @export
setReplaceMethod("dimnames", c("liger", "list"), function(x, value) {
    dataset.var <- x$dataset
    dataset.uniq <- unique(dataset.var)
    for (d in dataset.uniq) {
        dataset.idx <- dataset.var == d
        # Not using `dataset()` accessor because it does object validity check
        colnames(x@datasets[[d]]) <- value[[2L]][dataset.idx]
    }
    rownames(cellMeta(x)) <- value[[2L]]
    if (!is.null(x@H.norm)) rownames(x@H.norm) <- value[[2L]]
    x
})

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Subsetting ####
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#' Subset liger with brackets
#' @name sub-liger
#' @param x A \linkS4class{liger} object
#' @param i Feature subscriptor, passed to \code{featureIdx} of
#' \code{\link{subsetLiger}}.
#' @param j Cell subscriptor, passed to \code{cellIdx} of
#' \code{\link{subsetLiger}}.
#' @param ... Additional arguments passed to \code{\link{subsetLiger}}.
#' @export
#' @return Subset of \code{x} with specified features and cells.
#' @seealso \code{\link{subsetLiger}}
#' @method [ liger
#' @examples
#' pbmcPlot[varFeatures(pbmcPlot)[1:10], 1:10]
`[.liger` <- function(x, i, j, ...) {
    if (missing(i)) i <- NULL
    if (missing(j)) j <- NULL
    subsetLiger(x, featureIdx = i, cellIdx = j, ...)
}
# setMethod(
#     "[",
#     signature(x = "liger", i = "character", j = "missing"),
#     function(x, i, j, ...) subsetLiger(x, featureIdx = i, cellIdx = NULL, ...)
# )
#
# #' @export
# #' @rdname liger-class
# setMethod(
#     "[",
#     signature(x = "liger", i = "missing", j = "index"),
#     function(x, i, j, ...) subsetLiger(x, featureIdx = NULL, cellIdx = j, ...)
# )
#
# #' @export
# #' @rdname liger-class
# setMethod(
#     "[",
#     signature(x = "liger", i = "character", j = "index"),
#     function(x, i, j, ...) subsetLiger(x, featureIdx = i, cellIdx = j, ...)
# )

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Datasets ####
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#' @export
#' @rdname liger-class
setMethod("datasets", signature(x = "liger", check = "ANY"),
          function(x, check = NULL) {
              x@datasets
    })

#' @export
#' @rdname liger-class
setReplaceMethod("datasets", signature(x = "liger", check = "logical"),
                 function(x, check = TRUE, value) {
                     x@datasets <- value
                     if (isTRUE(check)) methods::validObject(x)
                     x
                 })

#' @export
#' @rdname liger-class
setReplaceMethod("datasets", signature(x = "liger", check = "missing"),
                 function(x, check = TRUE, value) {
                     x@datasets <- value
                     methods::validObject(x)
                     x
                 })



#' @export
#' @rdname liger-class
setMethod("dataset", signature(x = "liger", dataset = "character_OR_NULL"),
          function(x, dataset = NULL) {
              if (is.null(dataset)) return(datasets(x)[[1]])
              else {
                  if (!dataset %in% names(x)) {
                      cli::cli_abort("Specified dataset name {.val {dataset}} not found in {.cls liger} object")
                  }
                  return(datasets(x)[[dataset]])
              }
          })

#' @export
#' @rdname liger-class
setMethod("dataset", signature(x = "liger", dataset = "missing"),
          function(x, dataset = NULL) {
              datasets(x)[[1]]
          })

#' @export
#' @rdname liger-class
setMethod("dataset", signature(x = "liger", dataset = "numeric"),
          function(x, dataset = NULL) {
              datasets(x)[[dataset]]
          })

.mergeCellMeta <- function(cm1, cm2) {
    newDF <- S4Vectors::DataFrame(row.names = c(rownames(cm1), rownames(cm2)))
    for (var in names(cm1)) {
        value <- cm1[[var]]
        if (var %in% names(cm2)) {
            # TODO: check type
            tryCatch(
                expr = {
                    if (is.null(dim(value))) {
                        value <- c(value, cm2[[var]])
                    } else {
                        value <- rbind(value, cm2[[var]])
                    }
                },
                finally = {
                    cm2Idx <- seq(nrow(cm1) + 1, nrow(cm1) + nrow(cm2))
                    if (is.null(dim(value))) value[cm2Idx] <- NA
                    else {
                        empty <- matrix(NA, nrow = nrow(cm2), ncol = ncol(value))
                        value <- rbind(value, empty)
                    }
                }
            )
        } else {
            cm2Idx <- seq(nrow(cm1) + 1, nrow(cm1) + nrow(cm2))
            if (is.null(dim(value))) value[cm2Idx] <- NA
            else {
                empty <- matrix(NA, nrow = nrow(cm2), ncol = ncol(value))
                value <- rbind(value, empty)
            }
        }
        newDF[[var]] <- value
    }
    for (var in names(cm2)[!names(cm2) %in% names(cm1)]) {
        value <- cm2[[var]]
        if (is.null(dim(value))) {
            if (is.factor(value)) {
                value <- factor(c(rep(NA, nrow(cm1)), value),
                                levels = levels(value))
            } else {
                value <- c(rep(NA, nrow(cm1)), value)
            }
        } else {
            empty <- matrix(NA, nrow = nrow(cm1), ncol = ncol(value))
            value <- rbind(empty, value)
        }
        newDF[[var]] <- value
    }
    return(newDF)
}

.expandDataFrame <- function(df, idx) {
    dfList <- as.list(df)
    dfList <- lapply(dfList, function(x, idx) {
        if (is.null(dim(x))) {
            x[idx] <- NA
        } else {
            empty <- matrix(NA, nrow = length(idx), ncol = ncol(x))
            x <- rbind(x, empty)
        }
        x
    }, idx = idx)
    include <- sapply(dfList, function(x) {
        is.vector(x) | is.factor(x)
    })
    newdf <- S4Vectors::DataFrame(dfList[include])
    for (i in which(!include)) newdf[[names(dfList)[i]]] <- dfList[[i]]
    newdf
}

#' @export
#' @rdname liger-class
setReplaceMethod("dataset", signature(x = "liger", dataset = "character",
                                      type = "missing", qc = "ANY",
                                      value = "ligerDataset"),
                 function(x, dataset, type = NULL, qc = TRUE, value) {
                     if (dataset %in% names(x)) {
                         dataset(x, dataset) <- NULL
                     }
                     new.idx <- seq(ncol(x) + 1, ncol(x) + ncol(value))
                     x@datasets[[dataset]] <- value
                     cm <- x@cellMeta
                     remainingRowname <- rownames(cm)
                     cm <- .expandDataFrame(cm, new.idx)
                     rownames(cm) <- c(remainingRowname, colnames(value))

                     levels(cm$dataset) <- c(levels(cm$dataset), dataset)
                     cm$dataset[new.idx] <- dataset
                     cm$barcode[new.idx] <- colnames(value)
                     x@cellMeta <- cm
                     # x@W is genes x k, no need to worry
                     if (!is.null(x@H.norm)) {
                         cli::cli_alert_info("Filling in NAs to {.field H.norm} matrix")
                         H.normNew <- matrix(
                             NA, ncol(value), ncol(x@H.norm),
                             dimnames = list(colnames(value), NULL))
                         x@H.norm <- rbind(x@H.norm, H.normNew)
                     }
                     if (length(x@dimReds) != 0) {
                         cli::cli_alert_info("Filling in NAs to {.field dimReds}")
                         for (dr in names(x@dimReds)) {
                             x@dimReds[[dr]] <- rbind(
                                 x@dimReds[[dr]],
                                 matrix(NA, ncol(value), ncol(x@dimReds[[dr]]),
                                        dimnames = list(colnames(value), NULL))
                             )
                         }
                     }
                     methods::validObject(x)
                     if (qc) x <- runGeneralQC(x, organism = NULL,
                                               useDatasets = dataset,
                                               overwrite = TRUE,
                                               verbose = FALSE)
                     x
                 })

#' @export
#' @rdname liger-class
setReplaceMethod("dataset", signature(x = "liger", dataset = "character",
                                      type = "ANY", qc = "ANY",
                                      value = "matrixLike"),
                 function(x, dataset,
                          type = c("rawData", "normData"),
                          qc = FALSE,
                          value) {
                     type <- match.arg(type)
                     if (!all(startsWith(colnames(value), paste0(dataset, "_")))) {
                         cli::cli_alert_warning(
                             "Colnames of {.var value} do not all start with {.val {dataset}_}.
                                  Prefix added."
                         )
                         colnames(value) <- paste0(dataset, "_", colnames(value))
                     }
                     if (type == "rawData") {
                         ld <- createLigerDataset(rawData = value)
                     } else {
                         ld <- createLigerDataset(normData = value)
                     }
                     dataset(x, dataset, qc = qc) <- ld
                     x
                 })

#' @export
#' @rdname liger-class
setReplaceMethod(
    "dataset",
    signature(x = "liger", dataset = "character", type = "missing", qc = "ANY",
              value = "NULL"),
    function(x, dataset, type = NULL, qc = TRUE, value) {
        if (!dataset %in% names(x)) {
            cli::cli_alert_warning(
                "Specified dataset name {.val {dataset}} not found in {.cls liger} object. Nothing would happen.")
        } else {
            idxToRemove <- x$dataset == dataset
            x@datasets[[dataset]] <- NULL
            x@cellMeta <- x@cellMeta[!idxToRemove, , drop = FALSE]
            x@H.norm <- x@H.norm[!idxToRemove, , drop = FALSE]
            x@cellMeta$dataset <- droplevels(x@cellMeta$dataset)
            for (i in seq_along(x@dimReds)) {
                x@dimReds[[i]] <- x@dimReds[[i]][!idxToRemove, , drop = FALSE]
            }
        }
        x
    }
)

#' @rdname liger-class
#' @export
#' @method names liger
names.liger <- function(x) {
    names(x@datasets)
}

#' @rdname liger-class
#' @export
#' @method names<- liger
`names<-.liger` <- function(x, value) {
    originalNames <- names(x)
    if (identical(value, originalNames)) return(x)
    if (any(duplicated(value)))
        cli::cli_abort("Duplicated dataset names are not allowed.")

    dataset.idx <- lapply(originalNames, function(n) {
        x$dataset == n
    })
    x@cellMeta$dataset <- as.character(x@cellMeta$dataset)
    for (i in seq_along(value)) {
        x@cellMeta$dataset[dataset.idx[[i]]] <- value[i]
    }
    x@cellMeta$dataset <- factor(x@cellMeta$dataset, levels = value)
    names(x@datasets) <- value
    x
}

#' @rdname liger-class
#' @export
#' @method length liger
length.liger <- function(x) {
    .checkObjVersion(x)
    length(x@datasets)
}

#' @rdname liger-class
#' @export
#' @param use.names Whether returned vector should be named with dataset names.
#' @method lengths liger
lengths.liger <- function(x, use.names = TRUE) {
    .checkObjVersion(x)
    len <- sapply(x@datasets, ncol)
    if (isTRUE(use.names)) names(len) <- names(x@datasets)
    else names(len) <- NULL
    return(len)
}

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Cell metadata ####
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

.subsetCellMeta <- function(
        object,
        columns = NULL,
        cellIdx = NULL,
        as.data.frame = FALSE,
        ...
) {
    full <- object@cellMeta
    if (isTRUE(as.data.frame)) res <- .DataFrame.as.data.frame(full)
    else res <- full
    if (!is.null(columns)) {
        notFound <- !columns %in% colnames(res)
        if (any(notFound)) {
            cli::cli_alert_danger(
                "Specified variables from cellMeta not found: {.val {columns[notFound]}}")
            columns <- columns[!notFound]
        }
        res <- res[, columns, ...]
    }
    if (length(columns) == 1) {
        if (!is.null(dim(res))) {
            rownames(res) <- rownames(full)
        } else {
            names(res) <- rownames(full)
        }
    }
    if (!is.null(cellIdx)) {
        cellIdx <- .idxCheck(object, idx = cellIdx, orient = "cell")
        if (is.vector(res) || is.factor(res)) res <- res[cellIdx]
        else if (!is.null(dim(res))) res <- res[cellIdx, , ...]
        else cli::cli_abort("Result before idx subscription corrupted")
    }
    return(res)
}

#' @export
#' @rdname liger-class
setMethod(
    "cellMeta",
    signature(x = "liger", columns = "NULL"),
    function(x, columns = NULL, useDatasets = NULL, cellIdx = NULL, as.data.frame = FALSE, ...) {
        NULL
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "cellMeta",
    signature(x = "liger", columns = "character"),
    function(x, columns = NULL, useDatasets = NULL, cellIdx = NULL, as.data.frame = FALSE, ...) {
        if (is.null(cellIdx) && !is.null(useDatasets)) {
            if (!is.character(useDatasets)) useDatasets <- names(x)[useDatasets]
            cellIdx <- x@cellMeta$dataset %in% useDatasets
        }
        .subsetCellMeta(x, columns = columns, cellIdx = cellIdx,
                        as.data.frame = as.data.frame, ...)
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "cellMeta",
    signature(x = "liger", columns = "missing"),
    function(x, columns = NULL, useDatasets = NULL, cellIdx = NULL, as.data.frame = FALSE, ...) {
        if (is.null(cellIdx) && !is.null(useDatasets)) {
            if (!is.character(useDatasets)) useDatasets <- names(x)[useDatasets]
            cellIdx <- x@cellMeta$dataset %in% useDatasets
        }
        .subsetCellMeta(x, columns = NULL, cellIdx = cellIdx,
                        as.data.frame = as.data.frame, ...)
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "cellMeta",
    signature(x = "liger", columns = "missing"),
    function(x, columns = NULL, useDatasets = NULL, cellIdx = NULL, check = FALSE, value) {
        if (!inherits(value, "DFrame"))
            value <- S4Vectors::DataFrame(value)
        x@cellMeta <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
#' @param inplace For \code{cellMeta<-} method, when \code{columns} is for
#' existing variable and \code{useDatasets} or \code{cellIdx} indicate partial
#' insertion to the object, whether to by default (\code{TRUE}) in-place insert
#' \code{value} into the variable for selected cells or to replace the whole
#' variable with non-selected part left as NA.
setReplaceMethod(
    "cellMeta",
    signature(x = "liger", columns = "character"),
    function(x, columns = NULL, useDatasets = NULL, cellIdx = NULL,
             inplace = TRUE, check = FALSE, value) {
        # 1 - check cell selection
        if (is.null(cellIdx) && !is.null(useDatasets)) {
            if (!is.character(useDatasets)) useDatasets <- names(x)[useDatasets]
            cellIdx <- which(x@cellMeta$dataset %in% useDatasets)
        } else {
            cellIdx <- .idxCheck(x, cellIdx, "cell")
        }
        if (length(cellIdx) == 0)
            cli::cli_abort("No cell selected with either {.val cellIdx} or {.var useDatasets}.")

        # 2 - check value matching or length/dimension
        barcodes <- colnames(x)[cellIdx]
        if (is.null(dim(value))) {
            # Vector/factor like
            if (is.null(names(value))) {
                # No name matching, require exact length
                value <- .checkArgLen(value, n = length(cellIdx), class = c("vector", "factor"))
            } else {
                if (!all(endsWith(barcodes, names(value)))) {
                    cli::cli_abort(
                        c("x" = "{.code names(value)} do match to cells selected. ",
                          "i" = "The first three given names: {.val {names(value)[1:3]}}",
                          "i" = "The first three selected names: {.val {barcodes[1:3]}}")
                    )
                }
                names(value) <- barcodes
            }
        } else {
            # matrix like
            if (is.null(rownames(value))) {
                # no rowname matching, require extact nrow
                if (nrow(value) != length(cellIdx)) {
                    cli::cli_abort(
                        "{.code nrow(value)} ({nrow(value)}) does not match with cells selected ({length(cellIdx)}).")
                }
            } else {
                if (!all(barcodes %in% rownames(value))) {
                    cli::cli_abort(
                        c("{.code rownames(value)} do not contain all cells selected. ",
                          "These are not involved: ",
                          "{.val {barcodes[!barcodes %in% rownames(value)]}}")
                    )
                }
                value <- value[barcodes, , drop = FALSE]
            }
        }

        # 3 - Insert value
        if (length(cellIdx) == ncol(x)) {
            x@cellMeta[[columns]] <- value
        } else if (length(cellIdx) < ncol(x)) {
            # Partial replacement/addition
            if (!columns %in% names(x@cellMeta)) {
                # Add new variable
                if (is.null(dim(value))) {
                    # vector/factor
                    x@cellMeta[[columns]] <- NA
                    if (is.factor(value)) {
                        charValue <- as.character(value)
                        x@cellMeta[[columns]][cellIdx] <- charValue
                        x@cellMeta[[columns]] <- factor(x@cellMeta[[columns]],
                                                        levels = levels(value))
                    } else {
                        x@cellMeta[[columns]][cellIdx] <- value
                    }
                } else {
                    # matrix like
                    x@cellMeta[[columns]] <- matrix(
                        NA, ncol(x), ncol(value),
                        dimnames = list(colnames(x), colnames(value))
                    )
                    x@cellMeta[[columns]][cellIdx,] <- value
                }
            } else {
                if (isTRUE(inplace)) {
                    # Modifying existing variable
                    if (is.null(dim(value)) && is.null(dim(x@cellMeta[[columns]]))) {
                        # Both are 1-D
                        if (is.factor(value) && is.factor(x@cellMeta[[columns]])) {
                            charVar <- as.character(x@cellMeta[[columns]])
                            charVar[cellIdx] <- as.character(value)
                            x@cellMeta[[columns]] <-
                                factor(
                                    charVar,
                                    levels = unique(c(levels(x@cellMeta[[columns]]),
                                                      levels(value)))
                                )
                        } else {
                            x@cellMeta[[columns]][cellIdx] <- value
                        }
                    } else if (!is.null(dim(value)) && !is.null(dim(x@cellMeta[[columns]]))) {
                        # Both are dimensional
                        if (ncol(value) != ncol(x@cellMeta[[columns]])) {
                            cli::cli_abort("Cannot insert value to a variable of different dimensionality")
                        }
                        x@cellMeta[[columns]][cellIdx,] <- value
                    } else {
                        cli::cli_abort("Cannot insert value to a variable of different dimensionality")
                    }
                } else {
                    x@cellMeta[[columns]] <- NULL
                    # Remove and go to "Add new variable" case above
                    cellMeta(x, columns = columns, cellIdx = cellIdx, check = check) <- value
                }
            }
        } else {
            cli::cli_abort("{.var cellIdx} pointing to more cells than available")
        }
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)


#' @export
#' @rdname liger-class
setMethod("rawData", c("liger", "ANY"),
          function(x, dataset = NULL) {
              if (is.null(dataset)) {
                  getMatrix(x, slot = "rawData", returnList = TRUE)
              } else {
                  getMatrix(x, "rawData", dataset = dataset)
              }
          }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "rawData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "matrixLike_OR_NULL"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with in-memory data for H5 based object.")
        x@datasets[[dataset]]@rawData <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "rawData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "H5D"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (!isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with on-disk data for in-memory object.")
        x@datasets[[dataset]]@rawData <- value
        if (isTRUE(check)) methods::validObject(x@datasets[[dataset]])
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setMethod("normData", c("liger", "ANY"),
          function(x, dataset = NULL) {
              if (is.null(dataset)) {
                  getMatrix(x, slot = "normData", returnList = TRUE)
              } else {
                  getMatrix(x, "normData", dataset = dataset)
              }
          }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "normData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "matrixLike_OR_NULL"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with in-memory data for H5 based object.")
        x@datasets[[dataset]]@normData <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "normData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "H5D"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (!isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with on-disk data for in-memory object.")
        x@datasets[[dataset]]@normData <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "scaleData",
    signature(x = "liger", dataset = "ANY"),
    function(x, dataset = NULL) {
        if (is.null(dataset)) {
            getMatrix(x, slot = "scaleData", returnList = TRUE)
        } else {
            getMatrix(x, "scaleData", dataset = dataset)
        }
    }
)


#' @export
#' @rdname liger-class
setReplaceMethod(
    "scaleData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "matrixLike_OR_NULL"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with in-memory data for H5 based object.")
        x@datasets[[dataset]]@scaleData <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "scaleData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "H5D"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (!isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with on-disk data for in-memory object.")
        x@datasets[[dataset]]@scaleData <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "scaleData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "H5Group"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (!isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with on-disk data for in-memory object.")
        x@datasets[[dataset]]@scaleData <- value
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "scaleUnsharedData",
    signature(x = "liger", dataset = "character"),
    function(x, dataset) {
        scaleUnsharedData(dataset(x, dataset))
    }
)


#' @export
#' @rdname liger-class
setMethod(
    "scaleUnsharedData",
    signature(x = "liger", dataset = "numeric"),
    function(x, dataset) {
        scaleUnsharedData(dataset(x, dataset))
    }
)


#' @export
#' @rdname liger-class
setReplaceMethod(
    "scaleUnsharedData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "matrixLike_OR_NULL"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        ld <- dataset(x, dataset)
        scaleUnsharedData(ld) <- value
        x@datasets[[dataset]] <- ld
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "scaleUnsharedData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "H5D"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        ld <- dataset(x, dataset)
        scaleUnsharedData(ld) <- value
        x@datasets[[dataset]] <- ld
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "scaleUnsharedData",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "H5Group"),
    function(x, dataset = NULL, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        if (length(dataset) != 1) cli::cli_abort("Need to specify one dataset to insert.")
        if (!isH5Liger(x, dataset))
            cli::cli_abort("Cannot replace slot with on-disk data for in-memory object.")
        ld <- dataset(x, dataset)
        scaleUnsharedData(ld) <- value
        x@datasets[[dataset]] <- ld
        if (isTRUE(check)) methods::validObject(x)
        x
    }
)


#' @export
#' @rdname liger-class
setMethod(
    "getMatrix", signature(x = "liger"),
    function(x,
             slot = c("rawData", "normData", "scaleData",
                      "scaleUnsharedData", "H", "V", "U", "A", "B",
                      "W", "H.norm"),
             dataset = NULL,
             returnList = FALSE) {
        slot <- match.arg(slot)
        if (slot == "W") return(x@W)
        if (slot == "H.norm") return(x@H.norm)
        if (is.null(dataset)) {
            return(lapply(datasets(x), function(ld) getMatrix(ld, slot)))
        } else {
            if (length(dataset) == 1) {
                if (isTRUE(returnList)) {
                    result <- list(getMatrix(dataset(x, dataset), slot))
                    names(result) <- dataset
                    return(result)
                } else if (isFALSE(returnList))
                    return(getMatrix(dataset(x, dataset), slot))
            } else {
                lds <- datasets(x)[dataset]
                return(lapply(lds, function(ld) getMatrix(ld, slot)))
            }
        }
    })

#' @export
#' @rdname liger-class
setMethod("getH5File",
          signature = signature(x = "liger", dataset = "ANY"),
          function(x, dataset = NULL) {
              if (is.null(dataset)) dataset <- names(x)
              dataset <- .checkUseDatasets(x, dataset)
              results <- lapply(datasets(x)[dataset],
                                function(ld) h5fileInfo(ld, "H5File"))
              if (length(results) == 1) results <- results[[1]]
              results
          })


#' Get cell metadata variable
#' @name sub-sub-liger
#' @param x A \linkS4class{liger} object
#' @param i Name or numeric index of cell meta data to fetch
#' @param ... Anything that \code{S4Vectors::\link[S4Vectors]{DataFrame}}
#' method allows.
#' @export
#' @method [[ liger
#' @return If \code{i} is given, the selected metadata will be returned; if it
#' is missing, the whole cell metadata table in
#' \code{S4Vectors::\link[S4Vectors]{DataFrame}} class will be returned.
#' @examples
#' # Retrieve whole cellMeta
#' pbmc[[]]
#' # Retrieve a variable
#' pbmc[["dataset"]]
`[[.liger` <- function(x, i, ...) {
    if (missing(i)) return(cellMeta(x))
    else return(x@cellMeta[[i, ...]])
}

#' Set cell metadata variable
#' @rdname liger-class
#' @param i Name or numeric index of cell meta variable to be replaced
#' @param value Metadata value to be inserted
#' @return Input liger object updated with replaced/new variable in
#' \code{cellMeta(x)}.
#' @export
#' @method [[<- liger
#' @examples
#' cellMeta(pbmc)
#' # Add new variable
#' pbmc[["newVar"]] <- 1
#' cellMeta(pbmc)
#' # Change existing variable
#' pbmc[["newVar"]][1:3] <- 1:3
#' cellMeta(pbmc)
`[[<-.liger` <- function(x, i, value) {
    name <- if (is.character(i)) i else colnames(x@cellMeta)[i]
    if (name == "dataset") {
        cli::cli_abort(
            c("x" = "Cannot directly modify {.var dataset} variable in {.cls liger} object.",
              "i" = "Please use {.code names(x) <- value} instead.")
        )
    }
    x@cellMeta[[i]] <- value
    methods::validObject(x)
    return(x)
}

#' @export
#' @method .DollarNames liger
.DollarNames.liger <- function(x, pattern = "") {
    grep(pattern, colnames(x@cellMeta), value = TRUE)
}


#' @export
#' @rdname liger-class
#' @method $ liger
`$.liger` <- function(x, name) {
    if (!name %in% colnames(cellMeta(x))) NULL
    else cellMeta(x, columns = name)
}

#' @export
#' @rdname liger-class
#' @method $<- liger
`$<-.liger` <- function(x, name, value) {
    cellMeta(x, columns = name) <- value
    return(x)
}

#' @export
#' @rdname liger-class
#' @param droplevels Whether to remove unused cluster levels from the factor
#' object fetched by \code{defaultCluster()}. Default \code{FALSE}.
setMethod(
    "defaultCluster",
    signature = c(x = "liger", useDatasets = "ANY"),
    function(x, useDatasets = NULL, droplevels = FALSE, ...) {
        # No name given, retrieve default
        useDatasets <- .checkUseDatasets(x, useDatasets)
        name <- x@uns$defaultCluster
        if (is.null(name)) return(NULL)
        cluster <- cellMeta(x, name, x$dataset %in% useDatasets)
        names(cluster) <- colnames(x)[x$dataset %in% useDatasets]
        if (isTRUE(droplevels)) cluster <- droplevels(cluster)
        return(cluster)
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "defaultCluster",
    signature(x = "liger", value = "character"),
    function(x, name = NULL, useDatasets = NULL, ..., value) {
        if (length(value) == 1) {
            # If doing defaultCluster(obj) <- "someName"
            if (!is.null(name)) {
                cli::cli_alert_danger("Cannot have {.code name} when selecting a variable with {.code value}.")
            }
            name <- value
            if (!name %in% colnames(cellMeta(x))) {
                cli::cli_abort("Selected variable does not exist in {.code cellMeta(x)}.")
            }
            x@uns$defaultCluster <- name
        } else {
            # If doing defaultCluster(obj) <- c(blah, blah, blah, ...)
            defaultCluster(x, name, useDatasets) <- factor(value)
        }
        return(x)
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "defaultCluster",
    signature(x = "liger", value = "factor"),
    function(x, name = NULL, useDatasets = NULL, droplevels = TRUE, ..., value) {
        if (isTRUE(droplevels)) value <- droplevels(value)
        useDatasets <- .checkUseDatasets(x, useDatasets)
        cellIdx <- x$dataset %in% useDatasets
        if (length(value) != sum(cellIdx)) {
            cli::cli_abort("Length of {.code value} does not match with the number of cells.")
        }
        if (is.null(name)) {
            cli::cli_alert_info(
                c("Storing given cluster labels to {.code cellMeta(x)} field: ",
                  "{.val defaultCluster}.")
            )
            name <- "defaultCluster"
        }
        if (is.null(names(value))) names(value) <- colnames(x)[cellIdx]
        else {
            if (all(names(value) %in% colnames(x)[cellIdx])) {
                value <- value[colnames(x)[cellIdx]]
            } else {
                cli::cli_abort(
                    c(x = "Not all {.code names(value)} match with target cells: ",
                      "{.val {names(value)[!names(value) %in% colnames(x)[cellIdx]]}}",
                      i = "Please have a check or try {.code unname(value)}.")
                )
            }
        }
        cellMeta(x, name, cellIdx) <- value
        x@uns$defaultCluster <- name
        return(x)
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "defaultCluster",
    signature(x = "liger", value = "NULL"),
    function(x, name = NULL, useDatasets = NULL, ..., value) {
        x@uns$defaultCluster <- NULL
        if ("defaultCluster" %in% colnames(cellMeta(x))) {
            cellMeta(x, "defaultCluster") <- NULL
        }
        return(x)
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "dimReds",
    signature = c(x = "liger"),
    function(x) x@dimReds
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "dimReds",
    signature(x = "liger", value = "list"),
    function(x, value) {
        x@dimReds <- value
        methods::validObject(x)
        return(x)
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "dimRed",
    signature = c(x = "liger", name = "missing_OR_NULL"),
    function(x, name = NULL, useDatasets = NULL, cellIdx = NULL, ...) {
        name <- x@uns$defaultDimRed
        dimred <- NULL
        if (is.null(name)) {
            if (length(x@dimReds) > 0) {
                cli::cli_alert_warning(
                    "No default {.field dimRed} recorded. Returning the first available."
                )
                dimred <- dimRed(x, name = 1, useDatasets = useDatasets,
                                 cellIdx = cellIdx, ...)
            } else {
                cli::cli_abort(
                    "No {.field dimRed} available in this {.cls liger} object."
                )
            }
        } else {
            dimred <- dimRed(x, name = name, useDatasets = useDatasets,
                             cellIdx = cellIdx, ...)
        }
        return(dimred)
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "dimRed",
    signature = c(x = "liger", name = "index"),
    function(x, name, useDatasets = NULL, cellIdx = NULL, ...) {
        if (is.null(useDatasets) && is.null(cellIdx)) {
            cellIdx <- seq_len(ncol(x))
        } else if (!is.null(cellIdx)) {
            cellIdx <- .idxCheck(x, cellIdx, "cell")
        } else if (!is.null(useDatasets)) {
            useDatasets <- .checkUseDatasets(x, useDatasets)
            cellIdx <- which(x$dataset %in% useDatasets)
        }

        name <- .findDimRedName(x, name, stopOnNull = TRUE)
        dimred <- x@dimReds[[name]]
        dimred <- dimred[cellIdx, , drop = FALSE]
        rownames(dimred) <- colnames(x)[cellIdx]
        colnames(dimred) <- paste0(name, "_", seq_len(ncol(dimred)))
        return(dimred)
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "dimRed",
    signature(x = "liger", name = "index", value = "NULL"),
    function(x, name = NULL, useDatasets = NULL, cellIdx = NULL, ..., value = NULL) {
        name <- .findDimRedName(x, name, stopOnNull = TRUE, returnFirst = FALSE)
        x@dimReds[[name]] <- NULL
        if (name %in% x@uns$defaultDimRed) x@uns$defaultDimRed <- NULL
        return(x)
    }
)

#' @export
#' @rdname liger-class
#' @param asDefault Whether to set the inserted dimension reduction matrix as
#' default for visualization methods. Default \code{NULL} sets it when no
#' default has been set yet, otherwise does not change current default.
setReplaceMethod(
    "dimRed",
    signature(x = "liger", name = "character", value = "matrixLike"),
    function(x, name = NULL, useDatasets = NULL, cellIdx = NULL, asDefault = NULL, inplace = FALSE, ..., value) {
        if (is.null(useDatasets) && is.null(cellIdx)) {
            cellIdx <- seq_len(ncol(x))
        } else if (!is.null(cellIdx)) {
            cellIdx <- .idxCheck(x, cellIdx, "cell")
        } else if (!is.null(useDatasets)) {
            useDatasets <- .checkUseDatasets(x, useDatasets)
            cellIdx <- which(x$dataset %in% useDatasets)
        }

        if (!name %in% names(x@dimReds) || isFALSE(inplace)) {
            # Totally new thing or just replace
            init <- matrix(
                data = NA,
                nrow = ncol(x), ncol = ncol(value),
                dimnames = list(
                    colnames(x),
                    paste0(name, "_", seq_len(ncol(value)))
                )
            )
        } else {
            # Partial insertion
            init <- dimRed(x, name = name)
            if (ncol(init) != ncol(value)) {
                cli::cli_abort(
                    "Cannot partially insert {ncol(value)} columns to {ncol(init)} columns inplace, at {.field dimReds}: {.val {name}}"
                )
            }
        }

        value <- as.matrix(value)
        if (nrow(value) != length(cellIdx)) {
            cli::cli_abort(
                "{.code nrow(value)} does not match with the number of cells selected."
            )
        }
        if (is.null(rownames(value))) {
            cli::cli_alert_warning(
                "No rownames detected. Assume cells match to the same order as in the object."
            )
        } else {
            if (!all(endsWith(colnames(x)[cellIdx], rownames(value)))) {
                cli::cli_abort(
                    "Cell identifiers in {.var value} do not match to those in the object"
                )
            }
        }
        rownames(value) <- colnames(x)[cellIdx]
        colnames(value) <- paste0(name, "_", seq_len(ncol(value)))
        init[rownames(value), ] <- value
        x@dimReds[[name]] <- init
        if (is.null(asDefault)) {
            if (!is.null(x@uns$defaultDimRed)) asDefault <- FALSE
            else asDefault <- TRUE
        }
        if (isTRUE(asDefault)) defaultDimRed(x) <- name
        return(x)
    }
)

#' @export
#' @rdname liger-class
setMethod(
    "defaultDimRed",
    signature(x = "liger", useDatasets = "ANY", cellIdx = "ANY"),
    function(x, useDatasets = NULL, cellIdx = NULL) {
        name <- x@uns$defaultDimRed
        if (is.null(name)) return(NULL)
        else dimRed(x, name = name, useDatasets = useDatasets, cellIdx = cellIdx)
    }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "defaultDimRed",
    signature(x = "liger", value = "character"),
    function(x, value) {
        if (length(value) != 1) {
            cli::cli_abort("Can only set one {.field dimRed} as default.")
        }
        value <- .findDimRedName(x, value, stopOnNull = TRUE)
        x@uns$defaultDimRed <- value
        return(x)
    }
)

#' @export
#' @rdname liger-class
setMethod("varFeatures", signature(x = "liger"),
          function(x) x@varFeatures)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "varFeatures",
    signature(x = "liger", check = "ANY", value = "character"),
    function(x, check = TRUE, value) {
        x@varFeatures <- value
        for (d in names(x)) {
            ld <- dataset(x, d)
            featureMeta(ld, check = FALSE)$selected <- rownames(ld) %in% value
            datasets(x, check = FALSE)[[d]] <- ld
        }
        if (isTRUE(check)) {
            checkResult <- unlist(lapply(datasets(x), function(ld) {
                all(value %in% rownames(ld))
            }), use.names = FALSE)
            if (!all(checkResult)) {
                cli::cli_alert_warning(
                    "Not all variable features passed are found in datasets: {.val {names(x)[!checkResult]}}"
                )
            }
        }
        x
    }
)


#' @export
#' @rdname liger-class
setMethod("varUnsharedFeatures", signature(x = "liger"),
          function(x, dataset = NULL) {
              dataset <- .checkUseDatasets(x, dataset)
              vufList <- lapply(dataset, function(d) x@datasets[[d]]@varUnsharedFeatures)

              if (length(vufList) == 1) return(vufList[[1]])
              else {
                  names(vufList) <- dataset
                  return(vufList)
              }
          }
)

#' @export
#' @rdname liger-class
setReplaceMethod(
    "varUnsharedFeatures",
    signature(x = "liger", dataset = "ANY", check = "ANY", value = "character"),
    function(x, dataset, check = TRUE, value) {
        dataset <- .checkUseDatasets(x, dataset)
        x@datasets[[dataset]]@varUnsharedFeatures <- value
        if (isTRUE(check)) {
            if (!all(value %in% rownames(x@datasets[[dataset]]))) {
                cli::cli_alert_warning("Not all features passed are found in dataset {.val {dataset}}.")
            }
        }
        return(x)
    }
)

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# S3 methods ####
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#' @rdname liger-class
#' @export
#' @method fortify liger
fortify.liger <- function(model, data, ...) {
    df <- cellMeta(model, as.data.frame = TRUE)
    if (!is.null(model@H.norm)) df <- cbind(df, model@H.norm)
    drs <- Reduce(cbind, model@dimReds)
    if (!is.null(drs)) df <- cbind(df, drs)
    df
}

#' @section Combining multiple liger object: The list of \code{datasets} slot,
#' the rows of \code{cellMeta} slot and the list of \code{commands} slot will
#' be simply concatenated. Variable features in \code{varFeatures} slot will be
#' taken a union. The \eqn{W} and \eqn{H.norm} matrices are not taken into
#' account for now.
#' @rdname liger-class
#' @export
#' @method c liger
c.liger <- function(...) {
    objList <- list(...)
    if (any(sapply(objList, function(obj) !inherits(obj, "liger"))))
        cli::cli_abort("Can only combine {.cls liger} objects with {.fn c} method for now.")
    allNames <- unlist(lapply(objList, names))
    if (any(duplicated(allNames)))
        cli::cli_abort(
            c("x" = "Cannot combine {.cls liger} objects with duplicated dataset names.",
              "i" = "Dataset names of an individual {.cls liger} object can be modified with {.code names(x) <- value}.")
        )

    objList[[length(objList)]] <- recordCommand(objList[[length(objList)]])
    allDatasets <- list()
    allCellMeta <- NULL
    varFeatures <- character()
    allCommands <- list()
    for (i in seq_along(objList)) {
        obj <- objList[[i]]
        allDatasets <- c(allDatasets, datasets(obj))
        allCellMeta <- rbind(allCellMeta, obj@cellMeta)
        varFeatures <- union(varFeatures, varFeatures(obj))
        allCommands <- c(allCommands, obj@commands)
    }
    methods::new("liger", datasets = allDatasets, cellMeta = allCellMeta,
                 varFeatures = varFeatures, commands = allCommands,
                 version = utils::packageVersion("rliger"))
}


#' @export
#' @rdname liger-class
setMethod(
    "commands",
    signature(x = "liger", funcName = "ANY", arg = "ANY"),
    function(x, funcName = NULL, arg = NULL) {
        if (is.null(funcName)) return(names(x@commands))
        cmdIdx <- c()
        for (n in funcName) {
            pat <- paste0("^", n)
            cmdIdx <- c(cmdIdx, grep(pat, names(x@commands)))
        }
        cmdIdx <- sort(unique(cmdIdx))
        result <- x@commands[cmdIdx]

        if (length(result) == 1) result <- result[[1]]

        if (!is.null(arg)) {
            if (is.list(result))
                result <- lapply(result, function(cmd) cmd@parameters[arg])
            else result <- unlist(result@parameters[arg])
        }
        return(result)
    }
)


#' @rdname peak
#' @export
setMethod(
    "rawPeak",
    signature(x = "liger", dataset = "character"),
    function(x, dataset) {
        atac <- dataset(x, dataset)
        rawPeak(atac)
    }
)

#' @rdname peak
#' @export
setReplaceMethod(
    "rawPeak",
    signature(x = "liger", dataset = "character"),
    function(x, dataset, check = TRUE, value) {
        ld <- dataset(x, dataset)
        rawPeak(ld, check = check) <- value
        x@datasets[[dataset]] <- ld
        x
    }
)

#' @rdname peak
#' @export
setMethod(
    "normPeak",
    signature(x = "liger", dataset = "character"),
    function(x, dataset) {
        atac <- dataset(x, dataset)
        normPeak(atac)
    }
)

#' @rdname peak
#' @export
setReplaceMethod(
    "normPeak",
    signature(x = "liger", dataset = "character"),
    function(x, dataset, check = TRUE, value) {
        ld <- dataset(x, dataset)
        normPeak(ld, check = check) <- value
        x@datasets[[dataset]] <- ld
        x
    }
)


#' @rdname coordinate
#' @export
setMethod(
    "coordinate",
    signature(x = "liger", dataset = "character"),
    function(x, dataset) {
        spatial <- dataset(x, dataset)
        coordinate(spatial)
    }
)

#' @rdname coordinate
#' @export
setReplaceMethod(
    "coordinate",
    signature(x = "liger", dataset = "character"),
    function(x, dataset, check = TRUE, value) {
        ld <- dataset(x, dataset)
        coordinate(ld, check = check) <- value
        x@datasets[[dataset]] <- ld
        x
    }
)
