#' Detect the input data format for Venn diagram processing
#'
#' Examines the structure of \code{data} and determines which of the four
#' supported formats it conforms to:
#' \itemize{
#'   \item \code{"long"} — a data frame with one row per element-set pair.
#'   \item \code{"wide"} — a data frame with logical/0-1 columns per set.
#'   \item \code{"list"} — a named list of element vectors per set.
#'   \item \code{"venn"} — a pre-computed \code{VennPlotData} object.
#' }
#'
#' @keywords internal
#' @param data A data frame, a named list, or a \code{VennPlotData} object.
#'  Data frames are classified as \code{"long"} when a single non-\code{NULL}
#'  \code{group_by} is provided, or \code{"wide"} when \code{group_by} has two
#'  or more columns or is \code{NULL}.
#' @param group_by A character string specifying the column name(s) identifying
#'  the set membership.  A single non-\code{NULL} column indicates long format.
#'  Two or more columns, or \code{NULL}, indicate wide format.
#' @param id_by A character string specifying the column name that identifies
#'  individual elements.  Required when \code{group_by} is a single column and
#'  \code{data} is a data frame.  Ignored for other input types.
#' @return A character string indicating the detected data format:
#'  \code{"long"}, \code{"wide"}, \code{"list"}, or \code{"venn"}.  Stops with
#'  an error if the data does not match any recognised type.
detect_venn_datatype <- function(data, group_by = NULL, id_by = NULL) {
    if (inherits(data, "data.frame")) {
        if (length(group_by) < 2 && !is.null(group_by)) {
            return("long")
        } else {
            # length(group_by) > 1
            return("wide")
        }
    } else if (inherits(data, "VennPlotData")) {
        return("venn")
    } else if (is.list(data)) {
        return("list")
    }

    stop(
        "Invalid data type. Please provide a data frame, a list or a VennPlotData object generated by prepare_venn_data()."
    )
}

#' Prepare input data for Venn diagram rendering
#'
#' Converts data in any supported input format into a \code{VennPlotData}
#' object suitable for rendering by \pkg{ggVennDiagram}.  When
#' \code{in_form = "auto"} (the default), the format is detected via
#' \code{\link{detect_venn_datatype}()}.
#'
#' @section Input formats:
#' \enumerate{
#'   \item \strong{Long format} — a data frame with a grouping column
#'         (\code{group_by}) identifying the set and an ID column
#'         (\code{id_by}) identifying each element.  Multiple \code{group_by}
#'         columns are concatenated with \code{group_by_sep}.
#' \preformatted{
#' group_by id_by
#' A        a1
#' A        a2
#' B        a1
#' B        a3
#' }
#'   \item \strong{Wide format} — a data frame where each column represents a
#'         set and each row an element.  Values must be logical or
#'         \code{0}/\code{1}.  Columns specified in \code{group_by} define the
#'         sets; if \code{group_by} is \code{NULL}, all columns are used.
#' \preformatted{
#' A    B
#' TRUE TRUE
#' TRUE FALSE
#' FALSE TRUE
#' }
#'   \item \strong{List format} — a named list where each element is a vector
#'         of identifiers belonging to that set.
#' \preformatted{
#' list(A = c("a1", "a2"), B = c("a1", "a3"))
#' }
#'   \item \strong{VennPlotData} — a pre-computed object returned by a
#'         previous call to \code{prepare_venn_data()}.  Returned as-is.
#' }
#'
#' @keywords internal
#' @param data A data frame, a named list, or a \code{VennPlotData} object
#'  to be prepared for Venn diagram rendering.  See the \strong{Input formats}
#'  section for the expected structure of each format.
#' @param in_form A character string specifying the input format.  One of
#'  \code{"auto"} (default; detect automatically via
#'  \code{\link{detect_venn_datatype}()}), \code{"long"}, \code{"wide"},
#'  \code{"list"}, or \code{"venn"}.
#' @param group_by A character string (or vector) specifying the column name(s)
#'  identifying set membership.  For long-format data, a single column defines
#'  the set; multiple columns are concatenated with \code{group_by_sep}.  For
#'  wide-format data, these are the set columns (must be logical or 0/1); when
#'  \code{NULL}, all columns are used as sets.  Ignored for list and
#'  \code{VennPlotData} input.
#' @param group_by_sep A character string used to concatenate multiple
#'  \code{group_by} columns when \code{in_form = "long"}.  Default \code{"_"}.
#' @param id_by A character string specifying the column name that identifies
#'  individual elements.  Required for long-format data; ignored otherwise.
#' @return A \code{VennPlotData} object suitable for rendering by
#'  \pkg{ggVennDiagram}.
prepare_venn_data <- function(
    data,
    in_form = "auto",
    group_by = NULL,
    group_by_sep = "_",
    id_by = NULL
) {
    # if (!requireNamespace("ggVennDiagram", quietly = TRUE)) {
    #     stop("ggVennDiagram is required for Venn diagram and its data processing.")
    # }
    if (in_form == "auto") {
        in_form <- detect_venn_datatype(data, group_by, id_by)
    }
    if (in_form == "venn") {
        if (!is.null(group_by)) {
            warning(
                "The 'group_by' argument is ignored when the input data is already a 'VennPlotData' object.",
                immediate. = TRUE
            )
        }
        return(data)
    }

    if (in_form == "list") {
        if (!is.list(data)) {
            stop("The input data must be a list when the in_form is 'list'.")
        }
        listdata <- data
    } 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
        )
        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 <- colnames(data)
        }
        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(
                    "The columns in group_by must be logical or 0/1 when the in_form is 'wide'."
                )
            }
        }
        data$.id <- paste0("id", seq_len(nrow(data)))
        listdata <- lapply(group_by, function(g) {
            data[as.logical(data[[g]]), ".id", 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
            }
        }
    }
    ggVennDiagram::process_data(ggVennDiagram::Venn(listdata))
}

#' Atomic Venn / Euler diagram (internal)
#'
#' @description
#' Core implementation for drawing a Venn or Euler diagram that visualises
#' the overlap relationships among multiple sets.  Supports four input formats
#' (long, wide, list, and pre-computed \code{VennPlotData}) which are
#' normalised by \code{\link{prepare_venn_data}()} into the internal
#' \code{VennPlotData} representation used by \pkg{ggVennDiagram}.
#'
#' Intersection regions can be filled either by a continuous colour gradient
#' based on the element count (\code{fill_mode = "count"} or \code{"count_rev"})
#' or by blended set colours (\code{fill_mode = "set"}).  Region labels can
#' display the raw count, the percentage of total elements, both, nothing, or
#' a custom function result.  Set labels always show the set name and its
#' total element count.
#'
#' @section Architecture:
#' \enumerate{
#'   \item \strong{Data type detection} — \code{\link{detect_venn_datatype}()}
#'         identifies whether \code{data} is in long, wide, list, or
#'         \code{VennPlotData} format.
#'   \item \strong{Data preparation} — \code{\link{prepare_venn_data}()}
#'         converts the input into a \code{VennPlotData} object suitable for
#'         rendering by \pkg{ggVennDiagram}.
#'   \item \strong{Geometry extraction} — \pkg{ggVennDiagram} utilities
#'         (\code{venn_regionedge()}, \code{venn_setedge()},
#'         \code{venn_regionlabel()}, \code{venn_setlabel()}) compute polygon
#'         vertices and label positions for all sets and their intersections.
#'   \item \strong{Fill resolution} — When \code{fill_mode = "set"},
#'         \code{\link{palette_this}()} assigns colours to each set; the
#'         \code{blend_colors()} helper blends the colours of overlapping sets
#'         for each intersection region.  When \code{fill_mode = "count"} or
#'         \code{"count_rev"}, a continuous gradient is applied across regions
#'         via \code{scale_fill_gradientn()} with a colour bar legend.
#'   \item \strong{Label computation} — Region labels are formatted per the
#'         \code{label} parameter: raw count, percentage, both, none, or a
#'         custom function that receives a data frame with columns
#'         \code{"id"}, \code{"X"}, \code{"Y"}, \code{"name"}, \code{"item"},
#'         and \code{"count"}.  Set labels are formatted as
#'         \code{"setName\\n(count)"}.
#'   \item \strong{Plot assembly} — \code{geom_polygon()} draws filled
#'         regions; \code{geom_path()} draws set outlines; two
#'         \code{\link[ggrepel]{geom_text_repel}()} layers add region labels
#'         and set labels.  For \code{fill_mode = "set"}, colours are mapped
#'         directly (no aesthetic mapping, no legend); for count modes, a
#'         \code{scale_fill_gradientn()} continuous scale is used.
#'   \item \strong{Theme and coordinate system} — \code{coord_equal()}
#'         enforces a square aspect ratio.  The theme removes all axis text,
#'         ticks, titles, grid lines, and panel borders.  The x-axis
#'         expansion is widened to accommodate set label text width.
#'   \item \strong{Dimension calculation} —
#'         \code{\link{calculate_plot_dimensions}()} computes \code{height}
#'         and \code{width} attributes (in inches) from \code{base_height},
#'         \code{aspect.ratio}, and legend metrics.  When
#'         \code{fill_mode = "set"}, the legend position is forced to
#'         \code{"none"}.
#' }
#'
#' @inheritParams common_args
#' @inheritParams prepare_venn_data
#' @param group_by A character string (or vector) specifying the column name(s)
#'  that define the set membership.  For \code{in_form = "long"}, this is the
#'  grouping column; for \code{in_form = "wide"}, these are the set columns
#'  (must be logical or 0/1); when \code{NULL} and \code{data} is a data
#'  frame, all columns are treated as sets (wide format).
#' @param label A character string or function controlling the text shown in
#'  each intersection region.  One of:
#'  \itemize{
#'    \item \code{"count"} (default) — the raw count of elements in that region.
#'    \item \code{"percent"} — the percentage of the total element count.
#'    \item \code{"both"} — count and percentage on separate lines.
#'    \item \code{"none"} — no region labels are drawn.
#'    \item A \strong{function} — receives a data frame with columns
#'          \code{"id"}, \code{"X"}, \code{"Y"}, \code{"name"}, \code{"item"},
#'          and \code{"count"}, and must return a character vector of labels.
#'  }
#' @param label_fg A character string specifying the colour of the label text.
#' @param label_size A numeric value specifying the font size of the label
#'  text.  When \code{NULL} (the default), auto-sized at 3.5 for region labels
#'  and 4 for set labels, scaled by \code{base_size / 12}.
#' @param label_bg A character string specifying the background colour of the
#'  label text (passed to \code{\link[ggrepel]{geom_text_repel}()} as
#'  \code{bg.color}).  Default \code{"white"}.
#' @param label_bg_r A numeric value specifying the corner radius of the label
#'  background rectangle (passed as \code{bg.r}).  Default \code{0.1}.
#' @param fill_mode A character string specifying how intersection regions are
#'  coloured.  One of:
#'  \itemize{
#'    \item \code{"count"} — continuous gradient based on element count
#'          (default palette: \code{"Spectral"}).
#'    \item \code{"count_rev"} — continuous gradient with reversed count order.
#'    \item \code{"set"} — discrete blended colours per set combination
#'          (default palette: \code{"Paired"}).  No legend is drawn.
#'  }
#' @param fill_name A character string for the colour bar legend title when
#'  \code{fill_mode} is \code{"count"} or \code{"count_rev"}.  Ignored when
#'  \code{fill_mode = "set"}.
#' @return A \code{ggplot} object with \code{height} and \code{width}
#'  attributes (in inches) attached.
#' @keywords internal
#' @importFrom rlang %||%
#' @importFrom ggplot2 geom_polygon geom_path coord_equal aes scale_x_continuous labs
#' @importFrom ggrepel geom_text_repel
VennDiagramAtomic <- function(
    data,
    in_form = "auto",
    group_by = NULL,
    group_by_sep = "_",
    id_by = NULL,
    label = "count",
    label_fg = "black",
    label_size = NULL,
    label_bg = "white",
    label_bg_r = 0.1,
    fill_mode = "count",
    palreverse = FALSE,
    fill_name = NULL,
    aspect.ratio = 1,
    palette = ifelse(fill_mode == "set", "Paired", "Spectral"),
    palcolor = NULL,
    alpha = 1,
    theme = "theme_this",
    theme_args = list(),
    title = NULL,
    subtitle = NULL,
    legend.position = "right",
    legend.direction = "vertical",
    ...
) {
    ggplot <- if (getOption("plotthis.gglogger.enabled", FALSE)) {
        gglogger::ggplot
    } else {
        ggplot2::ggplot
    }
    # if (!requireNamespace("ggVennDiagram", quietly = TRUE)) {
    #     stop("ggVennDiagram (v1.5+) is required for Venn diagram.")
    # }
    base_size <- theme_args$base_size %||% 12
    text_size_scale <- base_size / 12

    s <- data$.split
    data$.split <- NULL
    fill_mode <- match.arg(fill_mode, c("count", "set", "count_rev"))
    data <- prepare_venn_data(data, in_form, group_by, group_by_sep, id_by)
    data_regionedge <- ggVennDiagram::venn_regionedge(data)
    data_setedge <- ggVennDiagram::venn_setedge(data)
    data_regionlabel <- ggVennDiagram::venn_regionlabel(data)
    data_regionlabel$.split <- if (is.null(s)) NULL else s[1]
    data_setlabel <- ggVennDiagram::venn_setlabel(data)
    # Calculate the fill colors for the regions when fill_mode is set
    if (fill_mode == "set") {
        # 1: red 2: blue 3: green 4: purple
        colors <- palette_this(
            data$setLabel$id,
            palette = palette,
            palcolor = palcolor,
            reverse = palreverse
        )
        ids <- unique(data_regionedge$id)
        # 1: red 2: blue 3: green 4: purple
        # 1/2: blend(red, blue) 1/3: blend(red, green) 1/4: blend(red, purple)
        # ...
        blended_colors <- sapply(ids, function(id) {
            parts <- strsplit(id, "/")[[1]]
            blend_colors(colors[parts])
        })
        # "1" being transformed to "1.1" after sapply
        names(blended_colors) <- ids
        data_regionedge$fill <- blended_colors[data_regionedge$id]
        data_setedge$color <- colors[data_setedge$id]
    }

    data_setlabel$label <- paste0(
        data_setlabel$name,
        "\n(",
        data_setlabel$count,
        ")"
    )
    if (identical(label, "percent")) {
        data_regionlabel$label <- scales::percent(
            data_regionlabel$count / sum(data_regionlabel$count)
        )
    } else if (identical(label, "both")) {
        data_regionlabel$label <- paste(
            data_regionlabel$count,
            "\n",
            scales::percent(
                data_regionlabel$count / sum(data_regionlabel$count)
            )
        )
    } else if (identical(label, "count")) {
        data_regionlabel$label <- data_regionlabel$count
    } else if (is.function(label)) {
        data_regionlabel$label <- label(data_regionlabel)
    } else if (!identical(label, "none")) {
        stop(
            "Invalid label argument. Possible values are 'count', 'percent', 'both', 'none' or a function."
        )
    }

    p <- ggplot()

    if (fill_mode == "set") {
        p <- p +
            geom_polygon(
                data = data_regionedge,
                aes(!!sym("X"), !!sym("Y"), group = !!sym("id")),
                fill = data_regionedge$fill,
                alpha = alpha
            ) +
            geom_path(
                data = data_setedge,
                aes(!!sym("X"), !!sym("Y"), group = !!sym("id")),
                color = data_setedge$color,
                show.legend = FALSE
            )
    } else {
        p <- p +
            geom_polygon(
                data = data_regionedge,
                aes(
                    !!sym("X"),
                    !!sym("Y"),
                    fill = !!sym("count"),
                    group = !!sym("id")
                ),
                alpha = alpha
            ) +
            scale_fill_gradientn(
                n.breaks = 3,
                colors = palette_this(
                    palette = palette,
                    palcolor = palcolor,
                    reverse = palreverse
                ),
                na.value = "grey80",
                guide = guide_colorbar(
                    title = fill_name %||% "",
                    frame.colour = "black",
                    ticks.colour = "black",
                    title.hjust = 0
                )
            ) +
            geom_path(
                data = data_setedge,
                aes(
                    !!sym("X"),
                    !!sym("Y"),
                    color = !!sym("id"),
                    group = !!sym("id")
                ),
                color = "grey20",
                show.legend = FALSE
            )
    }

    if (!identical(label, "none")) {
        p <- p +
            geom_text_repel(
                data = data_regionlabel,
                aes(!!sym("X"), !!sym("Y"), label = !!sym("label")),
                color = label_fg,
                bg.color = label_bg,
                bg.r = label_bg_r,
                size = label_size %||% text_size_scale * 3.5,
                point.size = NA,
                max.overlaps = 100,
                force = 0,
                min.segment.length = 0,
                segment.colour = "black"
            )
    }

    p <- p +
        geom_text_repel(
            data = data_setlabel,
            aes(!!sym("X"), !!sym("Y"), label = !!sym("label")),
            color = label_fg,
            bg.color = label_bg,
            bg.r = label_bg_r,
            size = label_size %||% text_size_scale * 4,
            fontface = "bold",
            point.size = NA,
            max.overlaps = 100,
            force = 0,
            min.segment.length = 0,
            segment.colour = "black"
        ) +
        labs(title = title, subtitle = subtitle) +
        coord_equal() +
        do_call(theme, theme_args) +
        ggplot2::theme(
            aspect.ratio = aspect.ratio,
            legend.position = legend.position,
            legend.direction = legend.direction,
            panel.grid.major = element_blank(),
            panel.grid.minor = element_blank(),
            axis.text = element_blank(),
            axis.ticks = element_blank(),
            axis.title = element_blank(),
            panel.border = element_blank()
        )

    maxchars <- max(sapply(data_setlabel$name, nchar))
    p <- p +
        scale_x_continuous(
            expand = expansion(
                add = 0.001 * maxchars * text_size_scale,
                mult = 0.1
            ),
        )

    dims <- calculate_plot_dimensions(
        base_height = 5.5,
        aspect.ratio = aspect.ratio,
        legend.position = if (fill_mode == "set") "none" else legend.position,
        legend.direction = legend.direction
    )

    attr(p, "height") <- dims$height
    attr(p, "width") <- dims$width

    return(p)
}


#' Venn / Euler diagram
#'
#' @description
#' Draws Venn or Euler diagrams that visualise the overlap relationships among
#' multiple sets.  Supports four input formats: long (one row per element-set
#' pair), wide (logical/0-1 columns per set), a named list (element vectors
#' per set), and a pre-computed \code{VennPlotData} object.
#'
#' Intersection regions can be filled by a continuous colour gradient encoding
#' the element count (\code{fill_mode = "count"} / \code{"count_rev"}) or by
#' blended set colours (\code{fill_mode = "set"}).  Region labels can display
#' counts, percentages, both, or a custom function.  Set labels always show
#' the set name and its total element count.
#'
#' Use \code{split_by} to produce separate Venn diagrams for each level of a
#' grouping variable.  Note that \code{split_by} is only supported when
#' \code{data} is a data frame (list and \code{VennPlotData} inputs cannot be
#' split).
#'
#' @section split_by Workflow:
#' When a non-\code{NULL} \code{split_by} is provided and the input is a
#' data frame:
#' \enumerate{
#'   \item \strong{Validation} — an error is raised if \code{data} is not a
#'         data frame (list and \code{VennPlotData} input cannot be split).
#'   \item \strong{Column resolution} — \code{split_by} is validated and
#'         optionally concatenated via \code{\link{check_columns}()} with
#'         \code{force_factor = TRUE} and \code{allow_multi = TRUE}.
#'   \item \strong{Data splitting} — the data frame is split by the unique
#'         levels of the \code{split_by} column, preserving factor level
#'         order.  Empty levels are dropped via \code{droplevels()}.
#'   \item \strong{Per-split colour and legend resolution} —
#'         \code{\link{check_palette}()}, \code{\link{check_palcolor}()},
#'         and \code{\link{check_legend}()} resolve per-split palettes,
#'         custom colours, legend positions, and legend directions.
#'   \item \strong{Atomic dispatch} — \code{\link{VennDiagramAtomic}()} is
#'         called for each subset.  When \code{title} is a function, it
#'         receives the split level name for dynamic title generation.
#'   \item \strong{Combination} — results are passed to
#'         \code{\link{combine_plots}()} which returns a combined
#'         \code{patchwork} object (when \code{combine = TRUE}) or a named
#'         list of individual ggplot objects (when \code{combine = FALSE}).
#' }
#'
#' @inheritParams common_args
#' @inheritParams VennDiagramAtomic
#' @param split_by The column(s) to split the data by and produce separate
#'  Venn diagrams per subgroup.  Only supported when \code{data} is a data
#'  frame.  Multiple columns are 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.  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
#'  when \code{combine = TRUE}.
#' @param byrow Logical; fill the combined layout by row (default \code{TRUE}).
#' @param axes,axis_titles Character strings specifying how axes and axis
#'  titles are handled across the combined layout.
#' @param guides A character string specifying how legends are collected
#'  across panels in the combined layout.
#' @param design A custom layout specification for the combined plot.
#'  Passed to \code{\link[patchwork]{wrap_plots}()}.
#' @return A \code{ggplot} object (single split), a \code{patchwork} object
#'  (combined sub-plots), 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{
#' set.seed(8525)
#' data <- list(
#'     A = sort(sample(letters, 8)),
#'     B = sort(sample(letters, 8)),
#'     C = sort(sample(letters, 8)),
#'     D = sort(sample(letters, 8))
#' )
#'
#' # Basic Venn diagram with count labels
#' VennDiagram(data)
#'
#' # Fill by set membership (blended colours)
#' VennDiagram(data, fill_mode = "set")
#'
#' # Show both count and percentage
#' VennDiagram(data, label = "both")
#'
#' # Custom label function using set names
#' VennDiagram(data, label = function(df) df$name)
#'
#' # Custom palette and transparency
#' VennDiagram(data, palette = "material-indigo", alpha = 0.6)
#' }
VennDiagram <- function(
    data,
    in_form = c("auto", "long", "wide", "list", "venn"),
    split_by = NULL,
    split_by_sep = "_",
    group_by = NULL,
    group_by_sep = "_",
    id_by = NULL,
    label = "count",
    label_fg = "black",
    label_size = NULL,
    label_bg = "white",
    label_bg_r = 0.1,
    fill_mode = "count",
    palreverse = FALSE,
    fill_name = NULL,
    palette = ifelse(fill_mode == "set", "Paired", "Blues"),
    palcolor = NULL,
    alpha = 1,
    theme = "theme_this",
    theme_args = list(),
    title = NULL,
    subtitle = NULL,
    legend.position = "right",
    legend.direction = "vertical",
    aspect.ratio = 1,
    combine = TRUE,
    nrow = NULL,
    ncol = NULL,
    byrow = TRUE,
    seed = 8525,
    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 (length(datas) > 1 || !identical(nm, "...")) {
                datas[[nm]]$.split <- nm
            }
            VennDiagramAtomic(
                datas[[nm]],
                in_form = in_form,
                group_by = group_by,
                group_by_sep = group_by_sep,
                id_by = id_by,
                label = label,
                label_fg = label_fg,
                label_size = label_size,
                label_bg = label_bg,
                label_bg_r = label_bg_r,
                fill_mode = fill_mode,
                palreverse = palreverse,
                fill_name = fill_name,
                palette = palette[[nm]],
                palcolor = palcolor[[nm]],
                alpha = alpha,
                theme = theme,
                theme_args = theme_args,
                title = title,
                subtitle = subtitle,
                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
    )
}
