#' Detect the type of the input data for an UpSet plot
#'
#' Inspects the structure of \code{data} and returns a classifier string that
#' downstream functions use to select the parsing strategy.  When \code{data}
#' is an \code{UpsetPlotData} object (from \code{\link{prepare_upset_data}()})
#' it short-circuits to \code{"upset"} immediately.
#'
#' @keywords internal
#' @param data A data frame or a named list of element vectors.
#' @param group_by A character string specifying the column name for the set-definition groups.
#'   Only meaningful when \code{data} is a data frame.
#' @param id_by A character string specifying the column name for instance identifiers.
#'   Required when \code{group_by} is a single column and \code{data} is a data frame.
#' @return A character string, one of \code{"long"}, \code{"wide"}, \code{"list"},
#'   or \code{"upset"}.
#'   \describe{
#'     \item{\code{"long"}}{Long-format data --- each row records one (set, element) pair.}
#'     \item{\code{"wide"}}{Wide-format data --- each row is an element and each set has its own logical/0-1 column.}
#'     \item{\code{"list"}}{A named list of element vectors.}
#'     \item{\code{"upset"}}{Already an \code{UpsetPlotData} object.}
#'   }
detect_upset_datatype <- function(data, group_by = NULL, id_by = NULL) {
    if (inherits(data, "UpsetPlotData")) {
        return("upset")
    }

    if (inherits(data, "data.frame")) {
        if (length(group_by) < 2 && !is.null(group_by)) {
            return("long")
        } else {
            # length(group_by) > 1
            return("wide")
        }
    }

    if (inherits(data, "list")) {
        return("list")
    }

    stop(
        "Invalid data type. Please provide a data frame, a list or an UpsetPlotData object generated by prepare_upset_data()."
    )
}


#' Prepare data for an UpSet plot
#'
#' Converts raw input (long data frame, wide data frame, named list, or an
#' existing \code{UpsetPlotData} object) into the internal format expected by
#' \code{\link{UpsetPlotAtomic}()}.  This is the data-preparation workhorse
#' that handles all input format parsing.
#'
#' @section Input formats:
#'
#' \strong{Long format} --- one row per (set, element) pair:
#' \preformatted{
#' group_by id_by
#' A        a1
#' A        a2
#' B        a1
#' B        a3
#' }
#' Requires both \code{group_by} and \code{id_by}.
#'
#' \strong{Wide format} --- each row is an element, each set has a logical or
#' 0/1 membership column:
#' \preformatted{
#' A      B
#' TRUE   TRUE
#' TRUE   FALSE
#' FALSE  TRUE
#' }
#' The set columns are identified by \code{group_by} (or all columns except
#' \code{id_by} when \code{group_by = NULL}).
#'
#' \strong{List format} --- a named list of element vectors:
#' \preformatted{
#' list(A = c("a1", "a2"), B = c("a1", "a3"))
#' }
#'
#' \strong{UpsetPlotData} --- a pre-processed object (returned by this function)
#' is returned unchanged (with a warning if \code{group_by} is also provided).
#'
#' @param data A data frame, a named list of element vectors, or an
#'   \code{UpsetPlotData} object.
#' @param in_form A character string specifying the input format.  One of
#'   \code{"auto"} (default; detect from \code{data} structure),
#'   \code{"long"}, \code{"wide"}, \code{"list"}, or \code{"upset"}.
#' @param group_by A character vector of column name(s) defining the sets.
#'   In long format, this is the column with set labels.  In wide format,
#'   these are the set-membership columns.  When \code{NULL} in wide format,
#'   all columns except \code{id_by} are used.  Ignored for list and upset
#'   inputs.
#' @param group_by_sep A character string to concatenate multiple
#'   \code{group_by} columns when \code{in_form = "long"}.  Default: \code{"_"}.
#' @param id_by A character string specifying the column name for instance
#'   identifiers.  Required for long format; optional for wide format (a
#'   synthetic \code{.id} column is created if omitted).
#' @param specific A logical value.  When \code{TRUE} (default), only specific
#'   intersections are returned (elements belonging exclusively to the shown
#'   set combination).  When \code{FALSE}, all overlapping items are included.
#'   See \url{https://github.com/gaospecial/ggVennDiagram/issues/64}.
#' @return An \code{UpsetPlotData} object --- a data frame with an
#'   \code{Intersection} list-column of set labels and one row per element.
#'   A \code{"group_order"} attribute preserves the original set order.
#' @keywords internal
#' @importFrom rlang sym
#' @importFrom dplyr distinct %>%
#' @importFrom utils getFromNamespace
#' @importFrom tidyr uncount
prepare_upset_data <- function(
    data,
    in_form = "auto",
    group_by = NULL,
    group_by_sep = "_",
    id_by = NULL,
    specific = TRUE
) {
    if (in_form == "auto") {
        in_form <- detect_upset_datatype(data, group_by, id_by)
    }

    if (in_form == "upset") {
        if (!is.null(group_by)) {
            warning(
                "The group_by argument is ignored when the input data is an UpsetPlotData object.",
                immediate. = TRUE
            )
        }
        return(data)
    }

    process_upset_data <- getFromNamespace(
        "process_upset_data",
        "ggVennDiagram"
    )
    if (in_form == "list") {
        listdata <- data
        group_order <- names(listdata)
    } else if (in_form == "long") {
        group_by <- check_columns(
            data,
            group_by,
            force_factor = TRUE,
            allow_multi = TRUE,
            concat_multi = TRUE,
            concat_sep = group_by_sep
        )
        group_order <- levels(data[[group_by]])
        listdata <- split(data[[id_by]], data[[group_by]])
    } else {
        # in_form == "wide"
        group_by <- check_columns(data, group_by, allow_multi = TRUE)
        if (is.null(group_by)) {
            group_by <- setdiff(colnames(data), id_by)
        }
        group_order <- group_by
        for (g in group_by) {
            # columns must be logical or 0/1
            if (!is.logical(data[[g]]) && !all(data[[g]] %in% c(0, 1))) {
                stop(
                    "[UpSetPlot] The columns in group_by must be logical or 0/1 when the in_form is 'wide', but column '",
                    g,
                    "' is not."
                )
            }
        }
        id_by <- check_columns(data, id_by)
        if (is.null(id_by)) {
            id_by <- ".id"
            data$.id <- paste0("id", seq_len(nrow(data)))
        } else {
            # check if id_by column values are unique
            if (any(duplicated(data[[id_by]]))) {
                stop(
                    "[UpSetPlot] The values in id_by column must be unique when the in_form is 'wide', but there are duplicated values in column '",
                    id_by,
                    "'."
                )
            }
        }
        listdata <- lapply(group_by, function(g) {
            data[as.logical(data[[g]]), id_by, drop = TRUE]
        })
        names(listdata) <- group_by
        for (nm in names(listdata)) {
            if (length(listdata[[nm]]) == 0) {
                warning(
                    "The group '",
                    nm,
                    "' has no elements, ignored.",
                    immediate. = TRUE
                )
                listdata[[nm]] <- NULL
            }
        }
    }

    data <- process_upset_data(
        ggVennDiagram::Venn(listdata),
        specific = specific
    )
    idnames <- paste0(
        as.character(data$left_data$set),
        " (",
        as.character(data$left_data$size),
        ")"
    )
    names(idnames) <- as.character(seq_along(idnames))

    sep <- ifelse(specific, "/", "~")
    data <- distinct(data$main_data, !!sym("id"), !!sym("size"))

    data$Intersection <- lapply(as.character(data$id), function(x) {
        idnames[strsplit(x, sep, fixed = TRUE)[[1]]]
    })

    out <- uncount(data, !!sym("size"))
    attr(out, "group_order") <- group_order
    out
}

#' Atomic UpSet plot (internal)
#'
#' @description
#' Core implementation for drawing a single UpSet plot.  This is the internal
#' workhorse behind the exported \code{\link{UpsetPlot}} function --- it takes a
#' **single** data partition (no \code{split_by} support) and returns a
#' \code{ggplot} object.  The plot comprises three visual components:
#' \itemize{
#'   \item A \strong{horizontal bar chart} (top) showing the size of each
#'         intersection, with bars filled by a gradient of intersection count.
#'   \item A \strong{set-size bar chart} (left) showing the total number of
#'         elements in each set (added automatically by \code{ggupset}).
#'   \item A \strong{combination matrix} (bottom) where rows are sets, columns
#'         are intersections, and dots indicate set membership.  Connected dots
#'         trace the intersection pattern.
#' }
#'
#' @section Architecture:
#' \enumerate{
#'   \item \strong{ggplot dispatch} --- selects \code{gglogger::ggplot} or
#'         \code{ggplot2::ggplot} based on
#'         \code{getOption("plotthis.gglogger.enabled")}.
#'   \item \strong{Data preparation} --- \code{\link{prepare_upset_data}()}
#'         converts the input into the internal \code{UpsetPlotData} format
#'         with an \code{Intersection} list-column (set-membership labels per
#'         element) and a \code{"group_order"} attribute for set ordering.
#'   \item \strong{Text scale} --- \code{base_size} from \code{theme_args}
#'         (default 12) drives a \code{text_size_scale} that proportionally
#'         scales the combination-matrix row gap, label sizes, and point sizes.
#'   \item \strong{Bar geom} --- \code{geom_bar()} counts rows per intersection.
#'         Bars are filled by count via \code{scale_fill_gradientn()} with
#'         colours from \code{\link{palette_this}()}.
#'   \item \strong{Bar labels} --- when \code{label = TRUE},
#'         \code{\link[ggrepel]{geom_text_repel}()} places count labels above
#'         each bar, with configurable foreground, background, and size.
#'   \item \strong{Scale x upset} --- \code{ggupset::scale_x_upset(...)} is
#'         called, returning two components: a \code{ScaleUpset} (index \code{[[1]]})
#'         and a \code{CoordCombMatrix} (index \code{[[2]]}).  Only the scale is
#'         applied; the bundled coord is discarded to avoid a "Coordinate
#'         system already present" conflict.
#'   \item \strong{Labels and theme} --- \code{labs()} sets title, subtitle,
#'         and axis text.  \code{do_call(theme, theme_args)} applies the chosen
#'         theme.  Aspect ratio, legend position, legend direction, and grid
#'         lines are configured.
#'   \item \strong{Combination matrix} --- \code{ggupset::axis_combmatrix()}
#'         builds the lower panel via an \code{override_plotting_function}:
#'         \itemize{
#'           \item Reorders rows to match \code{group_order} (reversed for
#'                 top-to-bottom set listing).
#'           \item Alternating row backgrounds via \code{geom_rect()} with
#'                 white / \code{#F7F7F7} fills.
#'           \item Set-membership dots (\code{geom_point()}) in black for
#'                 present and \code{#E0E0E0} for absent.
#'           \item Connecting lines (\code{geom_line()}) through observed
#'                 dots to trace the intersection pattern.
#'         }
#'   \item \strong{Matrix theme} --- \code{ggupset::theme_combmatrix()}
#'         applies the combination-matrix appearance with
#'         \code{combmatrix_gap} between rows (scaled by
#'         \code{text_size_scale}).
#'   \item \strong{Dimension calculation} ---
#'         \code{\link{calculate_plot_dimensions}()} computes height from
#'         \code{n_sets} and width from \code{n_intersections}, both passed
#'         via \code{...} (default 99).  \code{aspect.ratio} is used as a
#'         standard H/W coupling with \code{x_scale_factor = 0.6}.  An
#'         additional width adjustment accounts for set-label character width.
#' }
#'
#' @inheritParams common_args
#' @inheritParams prepare_upset_data
#' @param label A logical value.  When \code{TRUE} (default), count labels are
#'   displayed above each intersection bar via \code{\link[ggrepel]{geom_text_repel}()}.
#' @param label_fg A character string specifying the colour of the label text.
#'   Default: \code{"black"}.
#' @param label_size A numeric value specifying the size of the label text.
#'   Default: \code{NULL} (computed from \code{base_size / 12 * 3.5}).
#' @param label_bg A character string specifying the background fill colour of
#'   the label.  Default: \code{"white"}.
#' @param label_bg_r A numeric value specifying the corner radius of the label
#'   background, passed to \code{geom_text_repel(bg.r)}.  Default: \code{0.1}.
#' @param combmatrix_gap A numeric value specifying the gap between rows of the
#'   combination matrix, measured at \code{base_size = 12}.  The actual gap is
#'   scaled by \code{text_size_scale = base_size / 12}.  Default: \code{6}.
#' @param levels A character vector of set levels to include, in display order.
#'   When \code{NULL} (default), all detected sets are shown.  Accepted for
#'   compatibility but not currently used by the internal implementation.
#' @param ... Additional arguments passed to \code{\link[ggupset]{scale_x_upset}}.
#'   Useful options include \code{n_sets} and \code{n_intersections} to limit
#'   the number of shown sets / intersections.
#' @return A \code{ggplot} object with \code{height} and \code{width} attributes
#'   (in inches).
#' @keywords internal
#' @importFrom rlang %||% sym
#' @importFrom ggplot2 geom_bar labs guide_colorbar scale_fill_gradientn
#' @importFrom ggplot2 aes geom_rect geom_point geom_line scale_x_continuous expansion
#' @importFrom ggplot2 scale_fill_manual scale_color_manual guides element_blank unit
#' @importFrom ggrepel geom_text_repel
UpsetPlotAtomic <- function(
    data,
    in_form = "auto",
    group_by = NULL,
    group_by_sep = "_",
    id_by = NULL,
    label = TRUE,
    label_fg = "black",
    label_size = NULL,
    label_bg = "white",
    label_bg_r = 0.1,
    palette = "material-indigo",
    palcolor = NULL,
    palreverse = FALSE,
    alpha = 1,
    specific = TRUE,
    combmatrix_gap = 6,
    theme = "theme_this",
    theme_args = list(),
    title = NULL,
    subtitle = NULL,
    xlab = NULL,
    ylab = NULL,
    aspect.ratio = 0.6,
    legend.position = "right",
    legend.direction = "vertical",
    levels = NULL,
    ...
) {
    ggplot <- if (getOption("plotthis.gglogger.enabled", FALSE)) {
        gglogger::ggplot
    } else {
        ggplot2::ggplot
    }

    data <- prepare_upset_data(
        data,
        in_form,
        group_by,
        group_by_sep,
        id_by,
        specific
    )
    group_order <- attr(data, "group_order")

    base_size <- theme_args$base_size %||% 12
    text_size_scale <- base_size / 12

    p <- ggplot(data, aes(x = !!sym("Intersection"))) +
        geom_bar(
            aes(fill = after_stat(!!sym("count"))),
            alpha = alpha,
            color = "black",
            width = 0.5
        ) +
        scale_fill_gradientn(
            n.breaks = 3,
            colors = palette_this(
                palette = palette,
                palcolor = palcolor,
                reverse = palreverse
            ),
            na.value = "grey80",
            guide = guide_colorbar(
                title = "",
                alpha = alpha,
                frame.colour = "black",
                ticks.colour = "black",
                title.hjust = 0
            )
        )
    if (isTRUE(label)) {
        p <- p +
            geom_text_repel(
                aes(label = after_stat(!!sym("count"))),
                stat = "count",
                colour = label_fg,
                size = label_size %||% text_size_scale * 3.5,
                bg.color = label_bg,
                bg.r = label_bg_r,
                nudge_y = 0.08 * text_size_scale,
                point.size = NA,
                max.overlaps = 100,
                force = 0,
                min.segment.length = 0,
                segment.colour = NA
            )
    }
    upset_args <- list(...)
    ytrans <- upset_args$ytrans %||% "identity"

    # scale_x_upset() internally calls axis_combmatrix(), bundling a CoordCombMatrix into its
    # return list. We extract only the ScaleUpset (index [[1]]) and discard the bundled
    # CoordCombMatrix (index [[2]]), then add our own axis_combmatrix() once — avoiding
    # a second coord system replacement and its "Coordinate system already present" message.
    scale_x_upset_res <- ggupset::scale_x_upset(...)
    p <- p +
        labs(
            title = title,
            subtitle = subtitle,
            x = xlab %||% "",
            y = ylab %||% "Intersection size"
        ) +
        scale_x_upset_res[[1]] +
        do_call(theme, theme_args) +
        ggplot2::theme(
            aspect.ratio = aspect.ratio,
            legend.position = legend.position,
            legend.direction = legend.direction,
            panel.grid.major = element_line(colour = "grey80", linetype = 2)
        ) +
        # try to keep the group order in combmatrix
        ggupset::axis_combmatrix(
            sep = " // ",
            ytrans = ytrans,
            override_plotting_function = function(df) {
                # df$single_label <- factor(df$single_label, levels = group_order)
                # single_label is something like: 'C (5)''B (5)''D (5)''A (5)'
                # but group_order is something like: 'A', 'B', 'C', 'D'
                # so we need to extract the group name from single_label and match it with group_order
                df$group_name <- sapply(
                    as.character(df$single_label),
                    function(x) {
                        # extract the group name from single_label, which is the part before the first " ("
                        strsplit(x, " (", fixed = TRUE)[[1]][1]
                    }
                )
                df$group_name <- factor(
                    df$group_name,
                    levels = rev(group_order)
                )
                df <- df[order(df$group_name), , drop = FALSE]
                df$single_label <- factor(
                    df$single_label,
                    levels = unique(df$single_label)
                )
                ggplot(df, aes(x = !!sym("at"), y = !!sym("single_label"))) +
                    geom_rect(
                        aes(fill = !!sym("index") %% 2 == 0),
                        ymin = df$index - 0.5,
                        ymax = df$index + 0.5,
                        xmin = 0,
                        xmax = 1
                    ) +
                    geom_point(
                        aes(color = !!sym("observed")),
                        size = text_size_scale * 3
                    ) +
                    geom_line(
                        data = function(dat) dat[dat$observed, , drop = FALSE],
                        aes(group = !!sym("labels")),
                        linewidth = 1.2 * text_size_scale
                    ) +
                    ggplot2::ylab("") +
                    ggplot2::xlab("") +
                    scale_x_continuous(limits = c(0, 1), expand = c(0, 0)) +
                    scale_y_discrete(expand = c(0, 0)) +
                    scale_fill_manual(
                        values = c(`TRUE` = "white", `FALSE` = "#F7F7F7")
                    ) +
                    scale_color_manual(
                        values = c(`TRUE` = "black", `FALSE` = "#E0E0E0")
                    ) +
                    guides(color = "none", fill = "none") +
                    ggplot2::theme(
                        panel.background = element_blank(),
                        axis.text.x = element_blank(),
                        axis.text.y = element_text(
                            size = 12 * text_size_scale^0.7,
                            color = "black"
                        ),
                        axis.ticks.y = element_blank(),
                        axis.ticks.length = unit(0, "pt"),
                        axis.title.y = element_blank(),
                        axis.title.x = element_blank(),
                        axis.line = element_blank(),
                        panel.border = element_blank()
                    )
            }
        ) +
        ggupset::theme_combmatrix(
            combmatrix.label.extra_spacing = combmatrix_gap * text_size_scale
        )

    n_sets <- upset_args$n_sets %||% 99
    n_sets <- min(n_sets, length(unique(unlist(data$Intersection))))
    n_intersections <- upset_args$n_intersections %||% 99
    n_intersections <- min(n_intersections, length(unique(data$Intersection)))
    maxchars <- max(sapply(unique(unlist(data$Intersection)), nchar))

    # Height driven by number of sets; width driven by number of intersections.
    # x_scale_factor = 0.6 preserves the original per-intersection width (the original
    # default aspect.ratio = 0.6 was used as a width-per-intersection scale, not H/W ratio).
    # aspect.ratio is now used as the standard H/W ratio coupling.
    dims <- calculate_plot_dimensions(
        base_height = 4.5 + n_sets * 0.5,
        aspect.ratio = aspect.ratio,
        n_x = n_intersections,
        x_scale_factor = 0.6,
        legend.position = legend.position,
        legend.direction = legend.direction,
        legend_n = n_sets,
        legend_nchar = maxchars
    )

    attr(p, "height") <- dims$height
    attr(p, "width") <- dims$width + maxchars * 0.05

    p
}

#' UpSet Plot
#'
#' @description
#' Draws an UpSet plot visualising set intersections and set sizes.  The plot
#' comprises:
#' \itemize{
#'   \item A \strong{horizontal bar chart} showing the size of each
#'         intersection, filled by the intersection count.
#'   \item A \strong{combination matrix} (rows = sets, columns = intersections)
#'         with membership dots and connecting lines.
#'   \item A \strong{set-size bar chart} on the left of the matrix (added
#'         automatically by \code{ggupset}).
#' }
#'
#' The function accepts data in four formats:
#' \itemize{
#'   \item \strong{List} --- a named list of element vectors (one per set).
#'   \item \strong{Long} --- a data frame with one row per (set, element) pair.
#'   \item \strong{Wide} --- a data frame where each row is an element and each
#'         set has its own logical or 0/1 membership column.
#'   \item \strong{UpsetPlotData} --- a pre-processed object from
#'         \code{\link{prepare_upset_data}()}.
#' }
#'
#' Supports splitting into sub-plots via \code{split_by}, per-split colour
#' palettes and legend control, and combining sub-plots via \code{patchwork}.
#'
#' @section split_by Workflow:
#' When \code{split_by} is provided:
#' \enumerate{
#'   \item \strong{Guard} --- \code{split_by} is only supported for
#'         \code{data.frame} input.  If \code{data} is a list (or other
#'         non-data.frame type) and \code{split_by} is non-\code{NULL}, an
#'         error is raised.
#'   \item \strong{Column validation} --- \code{\link{check_columns}()} resolves
#'         the \code{split_by} column(s) with \code{force_factor = TRUE} and
#'         \code{allow_multi = TRUE}.  Multiple columns are concatenated with
#'         \code{split_by_sep}.
#'   \item \strong{Data splitting} --- empty factor levels in \code{split_by}
#'         are dropped via \code{\link[base]{droplevels}()}, then the data
#'         frame is split by \code{split_by} (level order is preserved).  If
#'         \code{split_by} is \code{NULL}, the data is wrapped in a
#'         single-element list with name \code{"..."}.
#'   \item \strong{Per-split resolution} --- \code{\link{check_palette}()},
#'         \code{\link{check_palcolor}()}, and \code{\link{check_legend}()}
#'         resolve per-split \code{palette}, \code{palcolor},
#'         \code{legend.position}, and \code{legend.direction}.
#'   \item \strong{Atomic dispatch} --- \code{UpsetPlotAtomic} is called for
#'         each split.  If \code{title} is a function, it receives the split
#'         level name and can generate dynamic titles.  When in wide mode
#'         (\code{in_form} is \code{"auto"} or \code{"wide"}) and
#'         \code{group_by} is \code{NULL}, the set columns are auto-detected
#'         as all columns except \code{id_by} and \code{split_by}.
#'   \item \strong{Combination} --- Results are combined via
#'         \code{\link{combine_plots}()} (when \code{combine = TRUE}) or
#'         returned as a named list.
#' }
#'
#' @inheritParams common_args
#' @inheritParams UpsetPlotAtomic
#' @param split_by The column(s) to split the data by and produce separate
#'   sub-plots.  Only supported for \code{data.frame} input (list input raises
#'   an error).  Multiple columns concatenated with \code{split_by_sep}.
#' @param split_by_sep A character string to separate concatenated
#'   \code{split_by} columns.  Default: \code{"_"}.
#' @param seed A numeric seed for reproducibility.  Passed to
#'   \code{\link{validate_common_args}()}.  Default: \code{8525}.
#' @param combine Logical; when \code{TRUE} (default), returns a combined
#'   \code{patchwork} object.  When \code{FALSE}, returns a named list of
#'   individual \code{ggplot} objects.
#' @param ncol,nrow Integer number of columns / rows for the combined layout
#'   (passed to \code{\link[patchwork]{wrap_plots}}).
#' @param byrow Logical; fill the combined layout by row.  Default \code{TRUE}
#'   (passed to \code{\link[patchwork]{wrap_plots}}).
#' @param axes A character string specifying how axes should be treated across
#'   the combined layout (passed to \code{\link[patchwork]{wrap_plots}}).
#' @param axis_titles A character string specifying how axis titles should be
#'   treated across the combined layout.  Defaults to \code{axes}.
#' @param guides A character string specifying how legends should be collected
#'   across panels (passed to \code{\link{combine_plots}()}).
#' @param design A custom layout design for the combined plot (passed to
#'   \code{\link{combine_plots}()}).
#' @return A \code{ggplot} object, a \code{patchwork} object, or a named list
#'   of \code{ggplot} objects (when \code{combine = FALSE}), each with
#'   \code{height} and \code{width} attributes in inches.
#' @export
#' @examples
#' \donttest{
#' # ---- list input -------------------------------------------------------
#' data <- list(
#'     A = 1:5,
#'     B = 2:6,
#'     C = 3:7,
#'     D = 4:8
#' )
#' UpsetPlot(data)
#' UpsetPlot(data, label = FALSE)
#' UpsetPlot(data, palette = "Reds", specific = FALSE)
#'
#' # ---- long-format data frame ------------------------------------------
#' data_long <- data.frame(
#'     group_by = factor(
#'          c(rep("A", 5), rep("B", 5), rep("C", 5), rep("D", 5)),
#'          levels = c("A", "B", "C", "D")
#'     ),
#'     id_by = c(1:5, 2:6, 3:7, 4:8)
#' )
#' UpsetPlot(data_long, in_form = "long", group_by = "group_by", id_by = "id_by")
#'
#' # ---- wide-format data frame ------------------------------------------
#' data <- data.frame(
#'     id = LETTERS[1:10],
#'     B = c(1, 0, 1, 1, 0, 0, 1, 0, 1, 0),
#'     A = c(1, 1, 1, 0, 0, 1, 0, 0, 1, 0),
#'     D = c(1, 0, 0, 1, 1, 0, 0, 1, 0, 1),
#'     C = c(0, 1, 1, 0, 1, 0, 1, 0, 1, 0)
#' )
#' UpsetPlot(data, in_form = "wide", id_by = "id", n_intersections = 4)
#' }
UpsetPlot <- function(
    data,
    in_form = c("auto", "long", "wide", "list", "upset"),
    split_by = NULL,
    split_by_sep = "_",
    group_by = NULL,
    group_by_sep = "_",
    id_by = NULL,
    label = TRUE,
    label_fg = "black",
    label_size = NULL,
    label_bg = "white",
    label_bg_r = 0.1,
    palette = "Blues",
    palcolor = NULL,
    palreverse = FALSE,
    alpha = 1,
    specific = TRUE,
    theme = "theme_this",
    theme_args = list(),
    title = NULL,
    subtitle = NULL,
    xlab = NULL,
    ylab = NULL,
    aspect.ratio = 0.6,
    legend.position = "right",
    legend.direction = "vertical",
    combine = TRUE,
    nrow = NULL,
    ncol = NULL,
    byrow = TRUE,
    seed = 8525,
    combmatrix_gap = 6,
    axes = NULL,
    axis_titles = axes,
    guides = NULL,
    design = NULL,
    ...
) {
    validate_common_args(seed)
    in_form <- match.arg(in_form)
    theme <- process_theme(theme)
    if (!is.null(split_by) && !inherits(data, "data.frame")) {
        stop("'split_by' is only available for data frame.")
    }
    split_by <- check_columns(
        data,
        split_by,
        force_factor = TRUE,
        allow_multi = TRUE,
        concat_multi = TRUE,
        concat_sep = split_by_sep
    )

    if (!is.null(split_by)) {
        data[[split_by]] <- droplevels(data[[split_by]])
        datas <- split(data, data[[split_by]])
        # keep the order of levels
        datas <- datas[levels(data[[split_by]])]
    } else {
        datas <- list(data)
        split_by <- names(datas) <- "..."
    }
    palette <- check_palette(palette, names(datas))
    palcolor <- check_palcolor(palcolor, names(datas))
    legend.direction <- check_legend(
        legend.direction,
        names(datas),
        "legend.direction"
    )
    legend.position <- check_legend(
        legend.position,
        names(datas),
        "legend.position"
    )

    plots <- lapply(
        names(datas),
        function(nm) {
            default_title <- if (length(datas) == 1 && identical(nm, "...")) {
                NULL
            } else {
                nm
            }
            if (is.function(title)) {
                title <- title(default_title)
            } else {
                title <- title %||% default_title
            }
            if (is.null(group_by) && (in_form %in% c("auto", "wide"))) {
                group_by <- setdiff(colnames(datas[[nm]]), c(id_by, split_by))
            }
            UpsetPlotAtomic(
                datas[[nm]],
                in_form = in_form,
                group_by = group_by,
                group_by_sep = group_by_sep,
                id_by = id_by,
                combmatrix_gap = combmatrix_gap,
                label = label,
                label_fg = label_fg,
                label_size = label_size,
                label_bg = label_bg,
                label_bg_r = label_bg_r,
                palette = palette[[nm]],
                palcolor = palcolor[[nm]],
                palreverse = palreverse,
                alpha = alpha,
                specific = specific,
                theme = theme,
                theme_args = theme_args,
                title = title,
                subtitle = subtitle,
                xlab = xlab,
                ylab = ylab,
                aspect.ratio = aspect.ratio,
                legend.position = legend.position[[nm]],
                legend.direction = legend.direction[[nm]],
                ...
            )
        }
    )

    names(plots) <- names(datas)

    combine_plots(
        plots,
        combine = combine,
        split_by = split_by,
        nrow = nrow,
        ncol = ncol,
        byrow = byrow,
        axes = axes,
        axis_titles = axis_titles,
        guides = guides,
        design = design
    )
}
