#' Plot Euler and Venn diagrams
#'
#' Plot diagrams fit with [euler()] and [venn()] using [grid::Grid()] graphics.
#' This
#' function sets up all the necessary plot parameters and computes
#' the geometry of the diagram. [plot.eulergram()], meanwhile,
#' does the actual plotting of the diagram. Please see the **Details** section
#' to learn about the individual settings for each argument.
#'
#' The only difference between [plot.euler()] and [plot.venn()] is that
#' `quantities` is set to `TRUE` by default in the latter and `FALSE` in
#' the former.
#'
#' Most of the arguments to this function accept either a logical, a vector, or
#' a list where
#'
#' * logical values set the attribute on or off,
#' * vectors are shortcuts to commonly used options (see the individual
#' parameters), and
#' * lists enable fine-grained control, including graphical
#' parameters as described in [grid::gpar()] and control
#' arguments that are specific to each argument.
#'
#' The various [grid::gpar()] values that are available for each argument
#' are:
#'
#' \tabular{lcccccccc}{
#'              \tab fills \tab edges \tab labels \tab quantities  \tab annotations \tab strips \tab legend \tab main \cr
#'   col        \tab       \tab x     \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#'   fill       \tab x     \tab       \tab        \tab             \tab             \tab        \tab        \tab      \cr
#'   alpha      \tab x     \tab x     \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#'   lty        \tab       \tab x     \tab        \tab             \tab             \tab        \tab        \tab      \cr
#'   lwd        \tab       \tab x     \tab        \tab             \tab             \tab        \tab        \tab      \cr
#'   lex        \tab       \tab x     \tab        \tab             \tab             \tab        \tab        \tab      \cr
#'   fontsize   \tab       \tab       \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#'   cex        \tab       \tab       \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#'   fontfamily \tab       \tab       \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#'   lineheight \tab       \tab       \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#'   font       \tab       \tab       \tab x      \tab x           \tab x           \tab x      \tab x      \tab x    \cr
#' }
#'
#' Defaults for these values, as well as other parameters of the plots, can
#' be set globally using [eulerr_options()].
#'
#' If the diagram has been fit using the `data.frame` or `matrix` methods
#' and using the `by` argument, the plot area will be split into panels for
#' each combination of the one to two factors. The `fills`, `patterns`, `edges`,
#' `labels`, `quantities`, and `annotations` arguments each accept an optional
#' `by_group` entry: a named list of override lists keyed by panel name (the
#' names of the fitted object). For multi-`by` fits the panel name is the
#' levels joined by `.`, e.g. `"Male.German"`. Panels not listed in `by_group`
#' use the top-level settings unchanged. Only graphical fields (and `rot` for
#' `labels`, `quantities`, and `annotations`) may be overridden per panel;
#' structural fields such as `quantities$type`, `quantities$format`,
#' `annotations$labels`, or named-by-subset `fills$fill` must be set at the
#' top level.
#'
#' For users who are looking to plot their diagram using another package,
#' all the necessary parameters can be collected if the result of this
#' function is assigned to a variable (rather than printed to screen).
#'
#' @param x an object of class `'euler'`, generated from [euler()]
#' @param fills a logical, vector, or list of graphical parameters for the fills
#'   in the diagram. Vectors are assumed to be colors for the fills.
#'   See [grid::grid.path()]. Named fill vectors can be matched in
#'   `fills$mode = "disjoint"` (default) or `fills$mode = "union"`.
#' @param patterns a logical, vector, or list of graphical parameters for
#'   fill patterns in the diagram. Vectors are assumed to be pattern types
#'   (currently `"stripes"` or `NA`), where `NA` means no pattern.
#'   Supported list items are `type`, `angle`, `col`, `lwd`, and `alpha`.
#'   Named pattern vectors can be matched in
#'   `patterns$mode = "disjoint"` (default) or `patterns$mode = "union"`.
#' @param edges a logical, vector, or list of graphical parameters for the edges
#'   in the diagram. Vectors are assumed to be colors for the edges.
#'   See [grid::grid.polyline()].
#' @param legend a logical scalar or list. If a list, the item `side` can be
#'   used to set the location of the legend and `symbol_size` can be used to scale
#'   the legend symbols independently of the text size. See
#'   [grid::grid.legend()].
#' @param labels a logical, vector, or list. Vectors are assumed to be
#'   text for the labels. See [grid::grid.text()]. In addition to the
#'   `grid::gpar()` fields, the following placement controls are
#'   supported (delegated to the `eunoia` Rust crate):
#'   `labels$placement` (`"raycast"` (default), `"force_directed"`, or
#'   `"elbow"`) selects the strategy used when a label does not fit
#'   inside its region. `"raycast"` and `"force_directed"` produce
#'   straight leader lines (the former places the label along the
#'   centroid→POI ray, the latter relaxes labels with a polygon-aware
#'   force solver). `"elbow"` produces d3-pie style orthogonal leaders,
#'   stacking exterior labels in left/right columns reached by a
#'   three-segment polyline. `labels$margin` (numeric) overrides the
#'   per-region margin between an exterior label and the diagram
#'   (default is half the larger of the label's width and height);
#'   `labels$tether` (`"poi"` (default) or `"boundary"`) chooses where
#'   the leader line attaches on the source region; `labels$gap`
#'   controls the visible gap between the leader tip and the label box
#'   edge — a bare numeric is interpreted as `lines` (font-relative),
#'   a [grid::unit()] is honored as given, and the default `NULL`
#'   tracks `eulerr_options()$padding` so the gap matches the spacing
#'   between label and quantity; `labels$leader` is a list (`col`,
#'   `alpha`, `lwd`, `lty`, `lex`) styling the leader line drawn from
#'   the tether to the exterior label. Strategy-specific knobs live in
#'   their own sublists: `labels$force_directed = list(iterations = ...)`
#'   sets the iteration cap for the force-directed solver, and
#'   `labels$elbow = list(min_gap = ...)` sets the minimum vertical
#'   centre-to-centre spacing between stacked label boxes in an elbow
#'   column.
#' @param quantities a logical, vector, or list. Vectors are assumed to be
#'   text for the quantities' labels, which by
#'   default are the original values in the input to [euler()]. In addition
#'   to plain vectors, `quantities$labels` can also be a named vector keyed by
#'   subset names (e.g., `"A"`, `"B"`, `"A&B"`), which is useful for supplying
#'   custom text for overlap regions. If `quantities$labels` is `NULL`,
#'   `quantities$format` can be used to control number formatting as a list with
#'   an item `fun` (a function such as [signif()] or [round()]) and optional
#'   extra arguments passed to that function (for example,
#'   `list(fun = prettyNum, big.mark = ",")`). `quantities$total` can be used to
#'   set an external
#'   denominator for percent/fraction quantities (instead of the plotted total).
#'   to arguments that apply to [grid::grid.text()], an argument `type` may
#'   also be used which should be a combination of `"counts"`, `"percent"`,
#'   and `"fraction"`. The first item will be printed first and the second
#'   will be printed thereafter inside brackets. The default is
#'   `type = "counts"`.
#'   For finer control over the rendered text, set `quantities$template` to
#'   a string with `{counts}`, `{percent}`, and/or `{fraction}` placeholders,
#'   for example `"{counts}\n{percent}"` to put the count and percentage on
#'   separate lines or `"n={counts} ({percent})"` for arbitrary layout. When
#'   `template` is set it overrides `type`; the set of placeholders in the
#'   template determines which values are computed.
#' @param annotations free-form per-region text rendered as a third
#'   stacked element below the quantity (or below the label when no
#'   quantity is drawn). Accepts a named character vector keyed by
#'   subset name (e.g. `c(A = "n = 12", "A&B" = "n = 3")`) as a
#'   shorthand for `list(labels = <vector>)`, or a list with `labels`
#'   plus [grid::gpar()] fields (`col`, `alpha`, `fontsize`, `cex`,
#'   `fontfamily`, `lineheight`, `font`, `rot`). Regions absent from
#'   `labels` are not annotated. The composite tag bbox grows to
#'   include the annotation, so exterior placement and leader lines
#'   adapt automatically. Defaults to slightly smaller text than
#'   `labels` / `quantities` (`cex = 0.8`).
#' @param strips a list, ignored unless the `'by'` argument
#'   was used in [euler()]. In addition to graphical parameters, this
#'   argument can include `labels = list(top = ..., left = ...)` for custom
#'   strip labels. Unnamed labels are interpreted in display order. Named
#'   labels are matched by factor levels and then reordered to display order.
#' @param bg a logical, character, or list controlling the background grob.
#'   Character values are interpreted as the background fill color.
#' @param complement a logical, character, or list controlling the
#'   container box and complement region for diagrams fit with
#'   `complement =` in [euler()]. `TRUE` (default) draws the container
#'   with a dashed outline (`lty = 2`), no fill, and the complement count
#'   inside the complement region. `FALSE` suppresses the container and
#'   its label entirely. A character value is treated as a fill color
#'   shorthand. A list accepts `fill`, `alpha`, `col`, `lty`, `lwd`,
#'   `lex` (outline + label gpar), `fontsize`, `cex`, `font`, `fontfamily`,
#'   `lineheight` (label only), and `label` (custom text — defaults to the
#'   complement count). Also accepts the same placement controls as
#'   `labels` (`placement`, `margin`, `tether`, `gap`, `leader`,
#'   `force_directed`, `elbow`)
#'   for the complement count label. Has no effect if the diagram was
#'   fit without `complement =`. Defaults can be set via
#'   `eulerr_options(complement = ...)`.
#' @param n number of vertices for the `edges` and `fills`
#' @param main a title for the plot in the form of a
#'   character, expression, list or something that can be
#'   sensibly converted to a label via [grDevices::as.graphicsAnnot()]. A
#'   list of length one can be provided, in which case its only element
#'   is used as the label. If a list of longer length is provided, an item
#'   named `'label'` must be provided (and will be used for the actual text).
#' @param ... parameters to update `fills` and `edges` with and thereby a shortcut
#'   to set these parameters
#'   [grid::grid.text()].
#' @param rotate a numeric value giving the angle in degrees by which to rotate
#'   the entire diagram layout. Positive values rotate counter-clockwise.
#'   Defaults to `0` (no rotation).
#' @param adjust_labels a logical. If `TRUE`, adjustment will be made to avoid
#'   overlaps or out-of-limits plotting of labels, quantities, and
#'   percentages.
#'
#' @seealso [euler()], [plot.eulergram()], [grid::gpar()],
#'   [grid::grid.polyline()], [grid::grid.path()],
#'   [grid::grid.legend()], [grid::grid.text()]
#'
#' @return Provides an object of class `'eulergram'` , which is a
#'   description of the diagram to be drawn. [plot.eulergram()] does the actual
#'   drawing of the diagram.
#' @export
#' @examples
#' fit <- euler(c("A" = 10, "B" = 5, "A&B" = 3))
#'
#' # Customize colors, remove borders, bump alpha, color labels white
#' plot(fit,
#'      fills = list(fill = c("red", "steelblue4"), alpha = 0.5),
#'      labels = list(col = "white", font = 4))
#'
#' # Add quantities to the plot
#' plot(fit, quantities = TRUE)
#'
#' # Add free-form per-region annotations below the counts
#' plot(
#'   fit,
#'   quantities = TRUE,
#'   annotations = c(A = "mean = 35", "A&B" = "mean = 41")
#' )
#'
#' # Add a custom legend and retain quantities
#' plot(fit, quantities = TRUE, legend = list(labels = c("foo", "bar")))
#'
#' # Plot without fills and distinguish sets with border types instead
#' plot(fit, fills = "transparent", lty = 1:2)
#'
#' # Save plot parameters to plot using some other method
#' diagram_description <- plot(fit)
#'
#' # Plots using 'by' argument
#' plot(euler(fruits[, 1:4], by = list(sex)), legend = TRUE)
#'
#' # Per-panel styling with `by_group`
#' plot(
#'   venn(fruits[, 1:4], by = list(sex)),
#'   quantities = list(
#'     by_group = list(
#'       male = list(col = "steelblue"),
#'       female = list(col = "tomato")
#'     )
#'   )
#' )
plot.euler <- function(
  x,
  fills = TRUE,
  patterns = FALSE,
  edges = TRUE,
  legend = FALSE,
  labels = identical(legend, FALSE),
  quantities = FALSE,
  annotations = NULL,
  strips = NULL,
  bg = FALSE,
  main = NULL,
  complement = TRUE,
  rotate = 0,
  n = 200L,
  adjust_labels = TRUE,
  ...
) {
  if (!missing(adjust_labels)) {
    warning("`adjust_labels` is deprecated and no longer has any effect.")
  }

  # retrieve default options
  opar <- eulerr_options()

  groups <- attr(x, "groups")
  dots <- list(...)

  do_custom_legend <- grid::is.grob(legend)

  do_fills <- !is_false(fills) && !is.null(fills)
  do_patterns <- !is_false(patterns) && !is.null(patterns)
  do_edges <- !is_false(edges) && !is.null(edges)
  do_labels <- !is_false(labels) && !is.null(labels)
  do_quantities <- !is_false(quantities) && !is.null(quantities)
  do_annotations <- !is_false(annotations) && !is.null(annotations)
  do_legend <- !is_false(legend) && !is.null(legend)
  do_groups <- !is.null(groups)
  do_strips <- !is_false(strips) && do_groups
  do_bg <- !is_false(bg) && !is.null(bg)
  do_main <- is.character(main) || is.expression(main) || is.list(main)

  has_container <- if (do_groups) {
    any(vapply(x, function(xi) !is.null(xi$container), logical(1)))
  } else {
    !is.null(x$container)
  }
  do_complement <- has_container &&
    !is_false(complement) &&
    !is.null(complement)

  # Extract per-panel overrides (`by_group`) from each styling parameter so the
  # existing normalization sees a clean list. These are re-applied later, once
  # per panel, inside the setup_grobs loop.
  panel_override_fields <- list(
    fills = c("fill", "alpha"),
    patterns = c("angle", "col", "lwd", "alpha"),
    edges = c("col", "alpha", "lex", "lwd", "lty"),
    labels = c(
      "col",
      "alpha",
      "fontsize",
      "cex",
      "fontfamily",
      "lineheight",
      "font",
      "rot"
    ),
    quantities = c(
      "col",
      "alpha",
      "fontsize",
      "cex",
      "fontfamily",
      "lineheight",
      "font",
      "rot"
    ),
    annotations = c(
      "col",
      "alpha",
      "fontsize",
      "cex",
      "fontfamily",
      "lineheight",
      "font",
      "rot"
    )
  )
  pop_by_group <- function(param, name) {
    if (!is.list(param) || is.null(param$by_group)) {
      return(list(param = param, by_group = NULL))
    }
    by_group <- param$by_group
    param$by_group <- NULL
    if (!do_groups) {
      stop(
        "`",
        name,
        "$by_group` requires a diagram fit with `by =`."
      )
    }
    if (!is.list(by_group) || length(by_group) == 0L) {
      stop("`", name, "$by_group` must be a non-empty named list.")
    }
    keys <- names(by_group)
    if (is.null(keys) || any(!nzchar(keys))) {
      stop("`", name, "$by_group` must be a fully named list.")
    }
    valid_keys <- names(x)
    unknown <- setdiff(keys, valid_keys)
    if (length(unknown) > 0L) {
      stop(
        "`",
        name,
        "$by_group` has unknown keys: ",
        paste(unknown, collapse = ", "),
        ". Valid keys: ",
        paste(valid_keys, collapse = ", "),
        "."
      )
    }
    allowed <- panel_override_fields[[name]]
    for (key in keys) {
      override <- by_group[[key]]
      if (!is.list(override)) {
        stop(
          "`",
          name,
          "$by_group$",
          key,
          "` must be a list."
        )
      }
      bad <- setdiff(names(override), allowed)
      if (length(bad) > 0L) {
        stop(
          "`",
          name,
          "$by_group$",
          key,
          "` contains fields that cannot be overridden per panel: ",
          paste(bad, collapse = ", "),
          ". Allowed fields: ",
          paste(allowed, collapse = ", "),
          "."
        )
      }
    }
    list(param = param, by_group = by_group)
  }

  fills_split <- pop_by_group(fills, "fills")
  fills <- fills_split$param
  fills_by_group <- fills_split$by_group

  patterns_split <- pop_by_group(patterns, "patterns")
  patterns <- patterns_split$param
  patterns_by_group <- patterns_split$by_group

  edges_split <- pop_by_group(edges, "edges")
  edges <- edges_split$param
  edges_by_group <- edges_split$by_group

  labels_split <- pop_by_group(labels, "labels")
  labels <- labels_split$param
  labels_by_group <- labels_split$by_group

  quantities_split <- pop_by_group(quantities, "quantities")
  quantities <- quantities_split$param
  quantities_by_group <- quantities_split$by_group

  annotations_split <- pop_by_group(annotations, "annotations")
  annotations <- annotations_split$param
  annotations_by_group <- annotations_split$by_group

  fills_user <- fills

  shapes <- if (do_groups) x[[1L]]$shapes else x$shapes

  n_e <- NROW(shapes)

  setnames <- rownames(shapes)
  setnames_orig <- setnames

  # Build a sparse combo_labels that places singletons (in input order) first,
  # then multi-set combos in cardinality + lexicographic order. This is the
  # working set of regions for the entire plot pipeline.
  if (do_groups) {
    all_labels <- unique(unlist(
      lapply(x, function(xi) names(xi$fitted.values)),
      use.names = FALSE
    ))
  } else {
    all_labels <- names(x$fitted.values)
  }
  singletons_present <- intersect(setnames_orig, all_labels)
  multi_labels <- setdiff(all_labels, singletons_present)
  multi_card <- lengths(strsplit(multi_labels, "&", fixed = TRUE))
  multi_labels <- multi_labels[order(multi_card, multi_labels)]
  combo_labels <- c(singletons_present, multi_labels)
  combo_sets <- strsplit(combo_labels, "&", fixed = TRUE)
  n_id <- length(combo_labels)

  # Sparse equivalent of the legacy bit_indexr `id` matrix: rows are populated
  # combinations (in `combo_labels` order), columns are sets (in `setnames_orig`
  # order). id[i, j] is TRUE iff combination i includes set j.
  if (n_id > 0L && n_e > 0L) {
    id <- t(vapply(
      combo_sets,
      function(s) setnames_orig %in% s,
      logical(n_e)
    ))
    if (n_id == 1L) {
      id <- matrix(id, nrow = 1L)
    }
    dimnames(id) <- list(combo_labels, setnames_orig)
  } else {
    id <- matrix(
      FALSE,
      nrow = n_id,
      ncol = n_e,
      dimnames = list(combo_labels, setnames_orig)
    )
  }

  align_fitted <- function(xi) {
    out <- xi$fitted.values[combo_labels]
    out[is.na(out)] <- 0
    out
  }

  if (do_groups) {
    res <- lapply(x, function(xi) is.na(xi$shapes$h))
    empty_sets <- apply(do.call(rbind, res), 2, all)

    empty_subsets <- if (any(empty_sets)) {
      rowSums(id[, empty_sets, drop = FALSE]) > 0
    } else {
      logical(n_id)
    }

    res <- lapply(x, function(xi) {
      fitted <- align_fitted(xi)[!empty_subsets]
      nonzero <- nonzero_fit(fitted)
      ifelse(is.na(nonzero), FALSE, nonzero)
    })

    nonzero <- apply(do.call(rbind, res), 2, any)
  } else {
    empty_sets <- is.na(x$shapes$h)
    empty_subsets <- if (any(empty_sets)) {
      rowSums(id[, empty_sets, drop = FALSE]) > 0
    } else {
      logical(n_id)
    }
    fitted <- align_fitted(x)[!empty_subsets]
    nonzero <- nonzero_fit(fitted)
    nonzero <- ifelse(is.na(nonzero), FALSE, nonzero)
  }

  merged_sets <- rep(FALSE, length(setnames))

  if (!do_groups && any(nonzero)) {
    n_overlaps <- integer(n_id)
    single_mass <- logical(n_e)
    nz <- nonzero_fit(align_fitted(x))

    for (i in seq_len(n_e)) {
      nzi <- nz & id[, i]

      if (sum(nzi) == 1) {
        n_overlaps[which(nzi)] <- n_overlaps[which(nzi)] + 1
      }

      single_mass[i] <- sum(nzi) == 1
    }

    complete_overlaps <- n_overlaps > 1
    co_idx <- which(complete_overlaps)

    if (length(co_idx) > 0L) {
      for (idx in co_idx) {
        merge_sets <- id[idx, ] & single_mass
        if (any(merge_sets)) {
          setnames[merge_sets] <- paste(setnames[merge_sets], collapse = ",")
          merged_sets[which(merge_sets)[length(which(merge_sets))]] <- TRUE
        }
      }
    }
  }

  stopifnot(n > 0, is.numeric(n) && length(n) == 1)

  fills_out <- NULL

  # setup fills
  if (do_fills) {
    fills_out <- replace_list(
      list(
        fill = opar$fills$fill,
        alpha = opar$fills$alpha,
        mode = opar$fills$mode
      ),
      if (is.list(fills)) {
        fills
      } else if (isTRUE(fills)) {
        list()
      } else {
        list(fill = fills)
      }
    )
    fills_out <- replace_list(fills_out, dots)
    fills_out$col <- "transparent"

    if (is.function(fills_out$fill)) {
      fills_out$fill <- fills_out$fill(n_e)
    }

    fill_names <- names(fills_out$fill)
    if (!is.null(fill_names)) {
      all_named <- all(nzchar(fill_names))
      any_named <- any(nzchar(fill_names))
      if (any_named && !all_named) {
        stop("`fills$fill` must be either fully named or fully unnamed.")
      }
      if (all_named) {
        if (!fills_out$mode %in% c("disjoint", "union")) {
          stop("`fills$mode` must be either 'disjoint' or 'union'.")
        }
        subset_names <- rownames(id)
        valid_fill_names <- c(setnames, subset_names)
        unknown <- setdiff(fill_names, valid_fill_names)
        if (length(unknown) > 0L) {
          stop(
            "`fills$fill` has unknown names: ",
            paste(unknown, collapse = ", ")
          )
        }

        default_fill <- opar$fills$fill
        if (is.function(default_fill)) {
          default_fill <- default_fill(n_e)
        }
        n_default <- length(default_fill)
        if (n_default == n_e && n_default != n_id) {
          per_set <- default_fill
          default_fill <- character(n_id)
          for (ii in seq_len(n_id)) {
            set_idx <- which(id[ii, ])
            if (length(set_idx) == 1L) {
              default_fill[ii] <- per_set[set_idx]
            } else if (length(set_idx) > 1L) {
              default_fill[ii] <- mix_colors(per_set[set_idx])
            }
          }
        } else if (n_default == 1L || n_default == n_id) {
          default_fill <- rep_len(default_fill, n_id)
        } else {
          stop("Default `fills$fill` must have length 1, n_sets, or n_subsets.")
        }

        fill_map <- default_fill
        names(fill_map) <- subset_names
        named_sets <- intersect(fill_names, setnames)
        if (identical(fills_out$mode, "union") && length(named_sets) > 0L) {
          for (set_name in named_sets) {
            fill_map[id[, set_name]] <- fills_out$fill[[set_name]]
          }
        }
        named_subsets <- intersect(fill_names, subset_names)
        fill_map[named_subsets] <- unname(fills_out$fill[named_subsets])
        fills_out$fill <- fill_map
      }
    }

    n_fills <- length(fills_out$fill)
    if (n_fills == n_e && n_fills != n_id) {
      per_set <- fills_out$fill
      fills_out$fill <- character(n_id)
      for (i in seq_len(n_id)) {
        set_idx <- which(id[i, ])
        if (length(set_idx) == 1L) {
          fills_out$fill[i] <- per_set[set_idx]
        } else if (length(set_idx) > 1L) {
          fills_out$fill[i] <- mix_colors(per_set[set_idx])
        }
      }
    } else if (!(n_fills %in% c(1L, n_id))) {
      stop("`fills$fill` must have length 1, n_sets, or n_subsets.")
    }
    fills <- list()
    fills_gp <- fills_out
    fills_gp$mode <- NULL
    fills$gp <- setup_gpar(fills_gp, list(), n_id)
  } else {
    fills <- NULL
  }

  legacy_patterns <- list(
    type = if (is.list(fills_user) && !is.null(fills_user$pattern)) {
      fills_user$pattern
    } else {
      NULL
    },
    angle = if (is.list(fills_user) && !is.null(fills_user$angle)) {
      fills_user$angle
    } else {
      NULL
    },
    col = if (is.list(fills_user) && !is.null(fills_user$pattern_col)) {
      fills_user$pattern_col
    } else {
      NULL
    },
    lwd = if (is.list(fills_user) && !is.null(fills_user$pattern_lwd)) {
      fills_user$pattern_lwd
    } else {
      NULL
    }
  )
  do_legacy_patterns <- any(!vapply(legacy_patterns, is.null, logical(1)))
  do_patterns <- do_patterns || do_legacy_patterns

  # setup patterns
  if (do_patterns && !do_fills) {
    stop("`patterns` requires `fills` to be enabled.")
  }

  if (do_patterns) {
    patterns_out <- replace_list(
      list(
        type = opar$patterns$type,
        angle = opar$patterns$angle,
        col = opar$patterns$col,
        lwd = opar$patterns$lwd,
        alpha = opar$patterns$alpha,
        mode = opar$patterns$mode
      ),
      if (is.list(patterns)) {
        patterns
      } else if (isTRUE(patterns)) {
        list(type = "stripes")
      } else {
        list(type = patterns)
      }
    )

    if (do_legacy_patterns) {
      warning(
        "`fills$pattern`, `fills$angle`, `fills$pattern_col`, and ",
        "`fills$pattern_lwd` are deprecated; use `patterns` instead."
      )
      if (!is.null(legacy_patterns$type)) {
        patterns_out$type <- legacy_patterns$type
      }
      if (!is.null(legacy_patterns$angle)) {
        patterns_out$angle <- legacy_patterns$angle
      }
      if (!is.null(legacy_patterns$col)) {
        patterns_out$col <- legacy_patterns$col
      }
      if (!is.null(legacy_patterns$lwd)) patterns_out$lwd <- legacy_patterns$lwd
    }

    patterns_out <- replace_list(patterns_out, dots)

    pattern_type_names <- names(patterns_out$type)
    if (!is.null(pattern_type_names)) {
      all_named <- all(nzchar(pattern_type_names))
      any_named <- any(nzchar(pattern_type_names))
      if (any_named && !all_named) {
        stop("`patterns$type` must be either fully named or fully unnamed.")
      }
      if (all_named) {
        if (!patterns_out$mode %in% c("disjoint", "union")) {
          stop("`patterns$mode` must be either 'disjoint' or 'union'.")
        }
        subset_names <- rownames(id)
        valid_pattern_names <- c(setnames, subset_names)
        unknown <- setdiff(pattern_type_names, valid_pattern_names)
        if (length(unknown) > 0L) {
          stop(
            "`patterns$type` has unknown names: ",
            paste(unknown, collapse = ", ")
          )
        }
        named_sets <- intersect(pattern_type_names, setnames)
        named_subsets <- intersect(pattern_type_names, subset_names)

        if (
          identical(patterns_out$mode, "union") &&
            all(pattern_type_names %in% setnames)
        ) {
          set_default <- rep_len(opar$patterns$type, n_e)
          names(set_default) <- setnames
          set_default[named_sets] <- unname(patterns_out$type[named_sets])
          patterns_out$type <- unname(set_default)
        } else {
          default_type <- opar$patterns$type
          n_default <- length(default_type)
          if (n_default == n_e && n_default != n_id) {
            per_set <- default_type
            default_type <- rep(NA_character_, n_id)
            for (ii in seq_len(n_id)) {
              set_idx <- which(id[ii, ])
              if (length(set_idx) == 1L) {
                default_type[ii] <- per_set[set_idx]
              }
            }
          } else if (n_default == 1L || n_default == n_id) {
            default_type <- rep_len(default_type, n_id)
          } else {
            stop(
              "Default `patterns$type` must have length 1, n_sets, or n_subsets."
            )
          }

          type_map <- default_type
          names(type_map) <- subset_names
          if (
            identical(patterns_out$mode, "union") && length(named_sets) > 0L
          ) {
            for (set_name in named_sets) {
              type_map[id[, set_name]] <- patterns_out$type[[set_name]]
            }
          }
          type_map[named_subsets] <- unname(patterns_out$type[named_subsets])
          patterns_out$type <- type_map
        }
      }
    }

    expand_pattern_param <- function(x, name, by_set, default = NULL) {
      n_x <- length(x)
      if (!(n_x %in% c(1L, n_e, n_id))) {
        stop("`patterns$", name, "` must have length 1, n_sets, or n_subsets.")
      }
      if (n_x == 1L || n_x == n_id) {
        return(rep_len(x, n_id))
      }

      out <- vector(mode = mode(x), length = n_id)
      for (ii in seq_len(n_id)) {
        active <- which(id[ii, ] & by_set)
        if (length(active) == 0L) {
          if (is.null(default)) {
            out[ii] <- x[1L]
          } else if (is.character(default)) {
            out[ii] <- default[1L]
          } else {
            out[ii] <- as.vector(default)[1L]
          }
        } else if (is.character(x)) {
          out[ii] <- if (length(active) == 1L) {
            x[active]
          } else {
            mix_colors(x[active])
          }
        } else {
          out[ii] <- mean(x[active])
        }
      }
      out
    }

    n_types <- length(patterns_out$type)
    if (!(n_types %in% c(1L, n_e, n_id))) {
      stop("`patterns$type` must have length 1, n_sets, or n_subsets.")
    }

    if (n_types %in% c(1L, n_id)) {
      pattern_mode <- "intersection"
      patterns_out$type <- rep_len(patterns_out$type, n_id)
      patterns_out$angle <- expand_pattern_param(
        patterns_out$angle,
        "angle",
        rep(TRUE, n_e)
      )
      patterns_out$col <- expand_pattern_param(
        patterns_out$col,
        "col",
        rep(TRUE, n_e),
        default = NA_character_
      )
      patterns_out$lwd <- expand_pattern_param(
        patterns_out$lwd,
        "lwd",
        rep(TRUE, n_e)
      )
      patterns_out$alpha <- expand_pattern_param(
        patterns_out$alpha,
        "alpha",
        rep(TRUE, n_e)
      )
    } else {
      pattern_mode <- "shape"
      patterns_out$type <- tolower(patterns_out$type)
      patterns_out$angle <- rep_len(patterns_out$angle, n_e)
      patterns_out$col <- rep_len(patterns_out$col, n_e)
      patterns_out$lwd <- rep_len(patterns_out$lwd, n_e)
      patterns_out$alpha <- rep_len(patterns_out$alpha, n_e)
    }

    patterns <- list()
    patterns$mode <- pattern_mode

    if (pattern_mode == "intersection") {
      patterns_out$type[is.na(patterns_out$type)] <- "none"
      patterns_out$type <- tolower(patterns_out$type)

      if (any(!patterns_out$type %in% c("none", "stripes"))) {
        stop("`patterns$type` must be one of: 'stripes', NA.")
      }

      if (all(is.na(patterns_out$col))) {
        patterns_out$col <- fills$gp$fill
      } else {
        na_col <- is.na(patterns_out$col) | patterns_out$col == "NA"
        patterns_out$col[na_col] <- fills$gp$fill[na_col]
      }

      patterns_gp <- patterns_out
      patterns_gp$mode <- NULL
      patterns$gp <- setup_gpar(patterns_gp, list(), n_id)
    } else {
      patterns_out$type[is.na(patterns_out$type)] <- "none"
      if (any(!patterns_out$type %in% c("none", "stripes"))) {
        stop("`patterns$type` must be one of: 'stripes', NA.")
      }
      if (all(is.na(patterns_out$col))) {
        patterns_out$col <- fills$gp$fill[seq_len(n_e)]
      } else {
        na_col <- is.na(patterns_out$col) | patterns_out$col == "NA"
        patterns_out$col[na_col] <- fills$gp$fill[which(na_col)]
      }
      patterns_set_gp <- patterns_out
      patterns_set_gp$mode <- NULL
      patterns$set_gp <- setup_gpar(patterns_set_gp, list(), n_e)
      patterns$gp <- patterns$set_gp
    }
  } else {
    patterns <- NULL
  }

  # setup edges
  if (do_edges) {
    if (isTRUE(edges)) {
      edges_out <- list()
    } else if (!is.list(edges)) {
      edges_out <- list(col = edges)
    } else {
      edges_out <- edges
    }

    edges <- list()
    edges$gp <- setup_gpar(
      list(
        col = opar$edges$col,
        alpha = opar$edges$alpha,
        lex = opar$edges$lex,
        lwd = opar$edges$lwd,
        lty = opar$edges$lty
      ),
      update_list(edges_out, dots),
      n_e
    )
  } else {
    edges <- NULL
  }

  # setup strips
  if (do_groups) {
    group_names <- lapply(groups, levels)
    n_levels <- sum(lengths(group_names))
  }

  if (do_strips) {
    strip_labels <- NULL
    if (is.list(strips) && "labels" %in% names(strips)) {
      strip_labels <- strips$labels
      strips$labels <- NULL

      if (!is.null(strip_labels)) {
        if (!is.list(strip_labels)) {
          stop(
            "`strips$labels` must be a list with optional `top` and `left` entries."
          )
        }
        strip_label_names <- names(strip_labels)
        if (is.null(strip_label_names) || any(!nzchar(strip_label_names))) {
          stop(
            "`strips$labels` must be a named list with optional `top` and `left` entries."
          )
        }
        bad_labels <- setdiff(strip_label_names, c("top", "left"))
        if (length(bad_labels) > 0L) {
          stop("`strips$labels` only supports `top` and `left` entries.")
        }
      }
    }

    strips <- list(
      gp = setup_gpar(opar$strips, strips, n_levels),
      groups = groups,
      labels = strip_labels
    )
  } else {
    strips <- NULL
  }

  # setup labels
  if (do_labels) {
    if (is.list(labels)) {
      labels <- update_list(
        list(labels = setnames, rot = opar$labels$rot),
        labels
      )
    } else if (isTRUE(labels)) {
      labels <- list(labels = setnames, rot = opar$labels$rot)
    } else {
      labels <- list(labels = labels, rot = opar$labels$rot)
    }

    # Pull placement-related fields out so they don't leak into gpar.
    placement_fields <- c(
      "placement",
      "margin",
      "tether",
      "gap",
      "leader",
      "force_directed",
      "elbow"
    )
    placement_user <- labels[intersect(names(labels), placement_fields)]
    labels[placement_fields] <- NULL

    labels$rot <- rep_len(labels$rot, n_e)
    labels$gp <- setup_gpar(
      list(
        col = opar$labels$col,
        alpha = opar$labels$alpha,
        fontsize = opar$labels$fontsize,
        cex = opar$labels$cex,
        fontfamily = opar$labels$fontfamily,
        lineheight = opar$labels$lineheight,
        font = opar$labels$font
      ),
      labels,
      n_e
    )

    labels$placement <- placement_user$placement %||%
      opar$labels$placement
    labels$margin <- placement_user$margin %||% opar$labels$margin
    labels$tether <- placement_user$tether %||% opar$labels$tether
    labels$gap <- placement_user$gap %||% opar$labels$gap
    labels$force_directed <- update_list(
      opar$labels$force_directed %||% list(),
      placement_user$force_directed %||% list()
    )
    labels$elbow <- update_list(
      opar$labels$elbow %||% list(),
      placement_user$elbow %||% list()
    )
    labels$leader <- update_list(
      opar$labels$leader %||% list(),
      placement_user$leader %||% list()
    )
  } else {
    labels <- NULL
  }

  # setup quantities
  if (do_quantities) {
    normalize_quantity_formatter <- function(formatter) {
      if (is.null(formatter)) {
        formatter <- list()
      }

      if (!is.list(formatter)) {
        stop("`quantities$format` must be a list.")
      }

      if (is.null(formatter$fun)) {
        fun <- NULL
      } else {
        fun <- formatter$fun
      }

      if (!is.null(fun) && !is.function(fun)) {
        stop("`quantities$format$fun` must be a function.")
      }

      args <- formatter[setdiff(names(formatter), "fun")]
      list(fun = fun, args = args)
    }

    if (is.list(quantities)) {
      if (!is.null(quantities$type)) {
        quantities$type[quantities$type == "numbers"] <- "counts"
        if (!all(quantities$type %in% c("counts", "percent", "fraction"))) {
          stop(
            "'type' must be one or more of 'counts', 'percent', and 'fraction'"
          )
        }

        quantities_type <- match.arg(
          quantities$type,
          c("counts", "percent", "fraction"),
          several.ok = TRUE
        )
      } else {
        quantities_type <- opar$quantities$type
      }

      quantities <- update_list(
        list(
          labels = NULL,
          type = quantities_type,
          template = opar$quantities$template,
          rot = opar$quantities$rot,
          format = NULL,
          total = NULL
        ),
        quantities
      )
    } else if (isTRUE(quantities)) {
      quantities <- list(
        labels = NULL,
        type = opar$quantities$type,
        template = opar$quantities$template,
        rot = opar$quantities$rot,
        format = NULL,
        total = NULL
      )
    } else {
      quantities <- list(
        labels = quantities,
        type = opar$quantities$type,
        template = opar$quantities$template,
        rot = opar$quantities$rot,
        format = NULL,
        total = NULL
      )
    }

    quantities$type[quantities$type == "numbers"] <- "counts"
    quantities$format <- normalize_quantity_formatter(quantities$format)

    if (!is.null(quantities$labels)) {
      lbl_names <- names(quantities$labels)
      if (!is.null(lbl_names)) {
        all_named <- all(nzchar(lbl_names))
        any_named <- any(nzchar(lbl_names))
        if (any_named && !all_named) {
          stop(
            "`quantities$labels` must be either fully named or fully unnamed."
          )
        }
      }
    }

    if (
      !is.null(quantities$total) &&
        (!is.numeric(quantities$total) ||
          length(quantities$total) != 1 ||
          is.na(quantities$total) ||
          quantities$total <= 0)
    ) {
      stop("`quantities$total` must be a single positive number.")
    }

    quantities$rot <- rep_len(quantities$rot, n_id)

    quantities$gp <- setup_gpar(
      list(
        col = opar$quantities$col,
        alpha = opar$quantities$alpha,
        fontsize = opar$quantities$fontsize,
        cex = opar$quantities$cex,
        fontfamily = opar$quantities$fontfamily,
        lineheight = opar$quantities$lineheight,
        font = opar$quantities$font
      ),
      quantities,
      n_id
    )
  } else {
    quantities <- NULL
  }

  # setup annotations
  if (do_annotations) {
    if (is.list(annotations)) {
      annotations <- update_list(
        list(labels = NULL, rot = opar$annotations$rot),
        annotations
      )
    } else if (isTRUE(annotations)) {
      annotations <- list(labels = NULL, rot = opar$annotations$rot)
    } else {
      annotations <- list(labels = annotations, rot = opar$annotations$rot)
    }

    if (!is.null(annotations$labels)) {
      if (!is.character(annotations$labels)) {
        stop("`annotations$labels` must be a character vector.")
      }
      lbl_names <- names(annotations$labels)
      if (is.null(lbl_names) || any(!nzchar(lbl_names))) {
        stop(
          "`annotations$labels` must be a fully named character vector keyed by subset (e.g. `c(A = \"...\", \"A&B\" = \"...\")`)."
        )
      }
    }

    annotations$rot <- rep_len(annotations$rot, n_id)

    annotations$gp <- setup_gpar(
      list(
        col = opar$annotations$col,
        alpha = opar$annotations$alpha,
        fontsize = opar$annotations$fontsize,
        cex = opar$annotations$cex,
        fontfamily = opar$annotations$fontfamily,
        lineheight = opar$annotations$lineheight,
        font = opar$annotations$font
      ),
      annotations,
      n_id
    )
  } else {
    annotations <- NULL
  }

  # setup legend
  if (do_custom_legend) {
    legend <- legend
  } else if (do_legend) {
    # TODO: create a better, custom legend

    legend <- update_list(
      list(
        labels = setnames[!empty_sets & !merged_sets],
        side = opar$legend$side,
        nrow = sum(!empty_sets),
        ncol = 1L,
        byrow = opar$legend$byrow,
        do.lines = opar$legend$do.lines,
        lines.first = opar$legend$lines.first,
        hgap = opar$legend$hgap,
        vgap = opar$legend$vgap,
        default.units = opar$legend$default.units,
        pch = opar$legend$pch,
        symbol_size = opar$legend$symbol_size
      ),
      legend
    )

    symbol_size <- if (!is.null(legend$symbol_size)) {
      legend$symbol_size
    } else {
      opar$legend$cex
    }
    legend$symbol_size <- NULL

    legend$gp <- setup_gpar(
      list(
        fill = if (do_fills) {
          fills$gp$fill[!empty_sets & !merged_sets]
        } else {
          "transparent"
        },
        alpha = if (do_fills) {
          fills$gp$alpha[!empty_sets & !merged_sets]
        } else if (do_edges) {
          edges$gp$alpha[!empty_sets & !merged_sets]
        } else {
          0
        },
        cex = opar$legend$cex,
        fontsize = opar$legend$fontsize,
        font = opar$legend$font,
        fontfamily = opar$legend$fontfamily,
        lwd = if (do_edges) edges$gp$lwd[!empty_sets & !merged_sets] else 0,
        lex = if (do_edges) edges$gp$lex[!empty_sets & !merged_sets] else 0,
        col = if (do_edges) {
          edges$gp$col[!empty_sets & !merged_sets]
        } else {
          "transparent"
        },
        pattern_type = if (do_patterns) {
          patterns$gp$type[!empty_sets & !merged_sets]
        } else {
          "none"
        },
        pattern_angle = if (do_patterns) {
          patterns$gp$angle[!empty_sets & !merged_sets]
        } else {
          45
        },
        pattern_col = if (do_patterns) {
          patterns$gp$col[!empty_sets & !merged_sets]
        } else {
          "transparent"
        },
        pattern_lwd = if (do_patterns) {
          patterns$gp$lwd[!empty_sets & !merged_sets]
        } else {
          0
        },
        pattern_alpha = if (do_patterns) {
          patterns$gp$alpha[!empty_sets & !merged_sets]
        } else {
          0
        }
      ),
      legend,
      sum(!empty_sets)
    )
  } else {
    legend <- NULL
  }

  if (do_main) {
    if (is.list(main)) {
      if (length(main) == 1 && is.null(names(main))) {
        label <- main[[1]]
      } else {
        label <- main$label
      }

      if (is.null(label)) {
        stop("you need to provide a 'label' item if 'main' is a list.")
      }
    } else {
      label <- main
    }

    main <- update_list(
      list(
        label = label,
        x = opar$main$x,
        y = opar$main$y,
        just = opar$main$just,
        hjust = opar$main$hjust,
        vjust = opar$main$vjust,
        rot = opar$main$rot,
        check.overlap = opar$main$check.overlap,
        default.units = opar$main$default.units
      ),
      main
    )

    main$gp <- setup_gpar(
      list(
        cex = opar$main$cex,
        fontsize = opar$main$fontsize,
        font = opar$main$font,
        fontfamily = opar$main$fontfamily,
        col = opar$main$col,
        lineheight = opar$main$lineheight,
        alpha = opar$main$alpha
      ),
      main,
      1
    )
  } else {
    main <- NULL
  }

  if (do_bg) {
    if (isTRUE(bg)) {
      bg <- list()
    } else if (!is.list(bg)) {
      bg <- list(fill = bg)
    }

    bg_grob <- grid::rectGrob(
      gp = setup_gpar(
        list(fill = "white", col = "transparent", alpha = 1),
        bg,
        1
      ),
      name = "bg.grob"
    )
  }

  # setup complement (container box + complement count label) styling.
  # The label text override `label` lives outside the gpar list so it isn't
  # passed to grid::gpar.
  if (do_complement) {
    complement_user <- if (is.list(complement)) {
      complement
    } else if (isTRUE(complement)) {
      list()
    } else {
      list(fill = complement)
    }
    complement_label <- complement_user$label
    complement_user$label <- NULL

    # Pull placement fields out so they don't leak into gpar.
    cplace_fields <- c(
      "placement",
      "margin",
      "tether",
      "gap",
      "leader",
      "force_directed",
      "elbow"
    )
    cplace_user <- complement_user[intersect(
      names(complement_user),
      cplace_fields
    )]
    complement_user[cplace_fields] <- NULL

    complement <- list(label = complement_label)
    complement$gp <- setup_gpar(
      list(
        fill = opar$complement$fill,
        alpha = opar$complement$alpha,
        col = opar$complement$col,
        lty = opar$complement$lty,
        lwd = opar$complement$lwd,
        lex = opar$complement$lex,
        fontsize = opar$complement$fontsize,
        cex = opar$complement$cex,
        font = opar$complement$font,
        fontfamily = opar$complement$fontfamily,
        lineheight = opar$complement$lineheight
      ),
      complement_user,
      1
    )
    complement$placement <- cplace_user$placement %||%
      opar$complement$placement
    complement$margin <- cplace_user$margin %||%
      opar$complement$margin
    complement$tether <- cplace_user$tether %||%
      opar$complement$tether
    complement$gap <- cplace_user$gap %||% opar$complement$gap
    complement$force_directed <- update_list(
      opar$complement$force_directed %||% list(),
      cplace_user$force_directed %||% list()
    )
    complement$elbow <- update_list(
      opar$complement$elbow %||% list(),
      cplace_user$elbow %||% list()
    )
    complement$leader <- update_list(
      opar$complement$leader %||% list(),
      cplace_user$leader %||% list()
    )
  } else {
    complement <- NULL
  }

  # apply layout rotation if requested
  if (rotate != 0) {
    theta <- rotate * pi / 180
    ct <- cos(theta)
    st <- sin(theta)
    # Rotation only updates `phi` for ellipse-like shapes — rectangles and
    # squares are axis-aligned in eunoia, so there's no in-plane rotation
    # angle to advance. The geometry itself is still rotated via the (h, k)
    # update, but the result rotates the centers without re-orienting the
    # boxes; this matches eunoia's axis-aligned model.
    rotate_shapes <- function(dd) {
      h_new <- dd$h * ct - dd$k * st
      dd$k <- dd$h * st + dd$k * ct
      dd$h <- h_new
      if (NROW(dd) > 0L) {
        type <- dd$type[1L]
        if (type %in% c("circle", "ellipse")) {
          dd$phi <- dd$phi + theta
        }
      }
      dd
    }
    rotate_ellipse_frame <- function(dd) {
      if (is.null(dd) || NROW(dd) == 0L) {
        return(dd)
      }
      h_new <- dd$h * ct - dd$k * st
      dd$k <- dd$h * st + dd$k * ct
      dd$h <- h_new
      dd$phi <- dd$phi + theta
      dd
    }
    if (do_groups) {
      x <- lapply(x, function(xi) {
        xi$shapes <- rotate_shapes(xi$shapes)
        if (!is.null(xi$ellipses)) {
          xi$ellipses <- rotate_ellipse_frame(xi$ellipses)
        }
        xi
      })
    } else {
      x$shapes <- rotate_shapes(x$shapes)
      if (!is.null(x$ellipses)) {
        x$ellipses <- rotate_ellipse_frame(x$ellipses)
      }
    }
  }

  # set up geometry for diagrams. Per-strategy sublists collapse to the
  # flat shape that `place_euler_labels` (and `apply_label_placement`'s
  # `placement_opts`) expect: `iterations` is only forwarded when
  # `force_directed` is the active strategy, `min_gap` only when `elbow`
  # is.
  strat <- labels$placement
  fd_opts <- labels$force_directed %||% list()
  el_opts <- labels$elbow %||% list()
  placement_opts <- list(
    placement = strat,
    margin = labels$margin,
    iterations = if (identical(strat, "force_directed")) {
      fd_opts$iterations
    } else {
      NULL
    },
    min_gap = if (identical(strat, "elbow")) el_opts$min_gap else NULL,
    tether = labels$tether,
    gap = labels$gap
  )
  if (do_groups) {
    data <- lapply(
      x,
      setup_geometry,
      fills = fills,
      edges = edges,
      labels = labels,
      quantities = quantities,
      annotations = annotations,
      n = n,
      merged_sets = merged_sets,
      placement_opts = placement_opts,
      do_complement_label = isTRUE(do_complement)
    )
  } else {
    data <- setup_geometry(
      x,
      fills = fills,
      edges = edges,
      labels = labels,
      quantities = quantities,
      annotations = annotations,
      n = n,
      merged_sets = merged_sets,
      placement_opts = placement_opts,
      do_complement_label = isTRUE(do_complement)
    )
  }

  # start setting up grobs

  if (do_groups) {
    n_groups <- length(data)
    group_keys <- names(x)
    panel_override <- function(by_group, key) {
      if (is.null(by_group)) {
        return(NULL)
      }
      by_group[[key]]
    }
    euler_grob_children <- grid::gList()
    for (i in seq_len(n_groups)) {
      key_i <- group_keys[i]
      fills_i <- apply_panel_overrides(
        fills,
        panel_override(fills_by_group, key_i)
      )
      patterns_i <- apply_panel_overrides(
        patterns,
        panel_override(patterns_by_group, key_i)
      )
      edges_i <- apply_panel_overrides(
        edges,
        panel_override(edges_by_group, key_i)
      )
      labels_i <- apply_panel_overrides(
        labels,
        panel_override(labels_by_group, key_i)
      )
      quantities_i <- apply_panel_overrides(
        quantities,
        panel_override(quantities_by_group, key_i)
      )
      annotations_i <- apply_panel_overrides(
        annotations,
        panel_override(annotations_by_group, key_i)
      )
      euler_grob_children[[i]] <- setup_grobs(
        data[[i]],
        fills = fills_i,
        patterns = patterns_i,
        edges = edges_i,
        labels = labels_i,
        quantities = quantities_i,
        annotations = annotations_i,
        complement = complement,
        number = i,
        merged_sets = merged_sets,
        n_vertices = as.integer(n),
        placement_opts = placement_opts
      )
    }
    euler_grob <- grid::gTree(
      grid::nullGrob(),
      name = "canvas.grob",
      children = euler_grob_children
    )
    pos <- vapply(groups, as.numeric, numeric(NROW(groups)), USE.NAMES = FALSE)
    layout <- lengths(lapply(groups, unique))
    if (length(layout) == 1L) {
      layout <- c(1L, layout)
    }
    xlim <- range(unlist(lapply(data, "[[", "xlim")))
    ylim <- range(unlist(lapply(data, "[[", "ylim")))
  } else {
    euler_grob <- setup_grobs(
      data,
      fills = fills,
      patterns = patterns,
      edges = edges,
      labels = labels,
      quantities = quantities,
      annotations = annotations,
      complement = complement,
      number = 1,
      merged_sets = merged_sets,
      n_vertices = as.integer(n),
      placement_opts = placement_opts
    )
    euler_grob <- grid::grobTree(euler_grob, name = "canvas.grob")

    xlim <- data$xlim
    ylim <- data$ylim
    pos <- c(1L, 1L)
    layout <- c(1L, 1L)
  }

  xrng <- abs(xlim[1L] - xlim[2L])
  yrng <- abs(ylim[1L] - ylim[2L])

  if (xrng == 0 || yrng == 0) {
    xrng <- yrng <- 1
    ylim <- xlim <- c(0, 1)
  }

  ar <- xrng / yrng

  # Reserve a fixed physical margin inside the canvas so shape outlines
  # (which straddle the geometric boundary by half a stroke width) aren't
  # clipped by the canvas edge. The narrower data axis gets the minimum
  # `panel_pad`; the wider axis is scaled up so the resulting inset
  # preserves the data aspect ratio (otherwise circles would render
  # slightly elliptical).
  panel_pad <- grid::unit(2, "pt")
  if (ar >= 1) {
    pad_x <- panel_pad * ar
    pad_y <- panel_pad
  } else {
    pad_x <- panel_pad
    pad_y <- panel_pad / ar
  }
  # adjust <- layout[1L]/layout[2]

  do_strip_left <- layout[1L] > 1L && do_strips
  do_strip_top <- layout[2L] > 1L && do_strips

  strip_top_row <- strip_top_col <- strip_left_row <- strip_left_col <- 1

  nrow <- ncol <- 1
  heights <- grid::unit(1, "null")
  widths <- grid::unit(1 * ar * layout[2] / layout[1], "null")
  diagram_col <- 1
  diagram_row <- 1

  if (do_main) {
    diagram_row <- diagram_row + 2
    nrow <- nrow + 2
    strip_left_row <- strip_left_row + 2
    strip_top_row <- strip_top_row + 2
  }

  if (do_strip_left) {
    diagram_col <- diagram_col + 1
    ncol <- ncol + 1
  }
  if (do_strip_top) {
    diagram_row <- diagram_row + 1
    nrow <- nrow + 1
  }

  if (do_strip_left && do_strip_top) {
    strip_top_col <- strip_top_col + 1
    strip_left_row <- strip_left_row + 1
  }

  # draw strips
  if (do_strip_top) {
    strip_top_vp <-
      grid::viewport(
        layout.pos.row = strip_top_row,
        layout.pos.col = strip_top_col,
        name = "strip.top.vp",
        layout = grid::grid.layout(nrow = 1, ncol = layout[2])
      )

    top_group_name <- names(layout)[[2L]]
    top_levels <- levels(strips$groups[[top_group_name]])
    lvls <- resolve_strip_labels(
      labels = strips$labels$top,
      levels = top_levels,
      display_levels = top_levels,
      axis = "top"
    )
    n_lvls <- length(lvls)
    step <- 1 / n_lvls

    strip_top_grob <- grid::textGrob(
      lvls,
      x = step / 2 + (seq(0, n_lvls - 1) * step),
      name = "strip.top.grob",
      gp = do.call(grid::gpar, strips$gp),
      vp = strip_top_vp
    )

    heights <- grid::unit.c(
      grid::unit(2, "grobheight", list(strip_top_grob)),
      heights
    )
  }

  if (do_strip_left) {
    strip_left_vp <-
      grid::viewport(
        layout.pos.row = strip_left_row,
        layout.pos.col = strip_left_col,
        name = "strip.left.vp",
        layout = grid::grid.layout(nrow = layout[1], ncol = 1)
      )

    left_group_name <- names(layout)[[1L]]
    left_levels <- levels(strips$groups[[left_group_name]])
    lvls <- resolve_strip_labels(
      labels = strips$labels$left,
      levels = left_levels,
      display_levels = rev(left_levels),
      axis = "left"
    )
    n_lvls <- length(lvls)
    step <- 1 / n_lvls

    strip_left_grob <- grid::textGrob(
      lvls,
      y = step / 2 + (seq(0, n_lvls - 1) * step),
      name = "strip.left.grob",
      rot = 90,
      gp = do.call(grid::gpar, strips$gp),
      vp = strip_left_vp
    )

    widths <- grid::unit.c(
      grid::unit(2, "grobwidth", list(strip_left_grob)),
      widths
    )
  }

  if (do_legend) {
    if (do_custom_legend) {
      legend_grob <- legend
      legend <- list(side = "right")
    } else {
      legend_grob <- euler_legend_grob(
        labels = legend$labels,
        gp = legend$gp,
        symbol_size = symbol_size,
        hgap = legend$hgap,
        vgap = legend$vgap,
        ncol = legend$ncol,
        nrow = legend$nrow,
        byrow = legend$byrow
      )
    }

    legend_grob$name <- "legend.grob"
    if (do_strip_top) {
      legend_row <- 2 + 2 * do_main
    }

    if (legend$side == "right") {
      # legend on right (default)
      ncol <- ncol + 2L
      legend_row <- nrow
      legend_col <- ncol
      widths <- grid::unit.c(
        widths,
        grid::unit(c(1, 1), c("lines", "grobwidth"), list(NULL, legend_grob))
      )
    } else if (legend$side == "left") {
      # legend on left
      ncol <- ncol + 2L
      legend_row <- if (do_strip_top) 2L + 2 * do_main else 1L + 2 * do_main
      legend_col <- 1
      diagram_col <- diagram_col + 2L
      if (do_strip_left) {
        strip_left_col <- strip_left_col + 2L
      }
      if (do_strip_top) {
        strip_top_col <- strip_top_col + 2L
      }
      widths <- grid::unit.c(
        grid::unit(c(1, 1), c("grobwidth", "lines"), list(legend_grob, NULL)),
        widths
      )
    } else if (legend$side == "top") {
      # legend on top
      nrow <- nrow + 2L
      legend_row <- 1L + do_main * 2
      legend_col <- if (do_strip_left) 2L else 1L
      diagram_row <- diagram_row + 2L
      if (do_strip_top) {
        strip_top_row <- strip_top_row + 2L
      }
      if (do_strip_left) {
        strip_left_row <- strip_left_row + 2L
      }
      heights <- grid::unit.c(
        grid::unit(c(1, 1), c("grobheight", "lines"), list(legend_grob, NULL)),
        heights
      )
    } else {
      # legend on bottom
      nrow <- nrow + 2L
      legend_row <- nrow
      legend_col <- if (do_strip_left) 2L else 1L
      heights <- grid::unit.c(
        heights,
        grid::unit(c(1, 1), c("lines", "grobheight"), list(NULL, legend_grob))
      )
    }
    legend_grob$vp <- grid::viewport(
      layout.pos.row = legend_row,
      layout.pos.col = legend_col,
      name = "legend.vp"
    )
  }

  if (do_main) {
    main_grob <- grid::textGrob(
      label = main$label,
      x = main$x,
      y = main$y,
      just = main$just,
      hjust = main$hjust,
      vjust = main$vjust,
      rot = main$rot,
      check.overlap = FALSE,
      default.units = main$default.units,
      gp = main$gp,
      name = "main.grob"
    )
    heights <- grid::unit.c(
      grid::unit(c(1, 1), c("lines", "grobheight"), list(NULL, main_grob)),
      heights
    )
    main_grob$vp <- grid::viewport(
      layout.pos.row = 1,
      layout.pos.col = diagram_col,
      name = "main.vp"
    )
  }

  canvas_vp <- grid::viewport(
    layout.pos.row = diagram_row,
    layout.pos.col = diagram_col,
    width = grid::unit(1, "npc") - pad_x - pad_x,
    height = grid::unit(1, "npc") - pad_y - pad_y,
    name = "canvas.vp",
    layout = grid::grid.layout(
      nrow = layout[1L],
      ncol = layout[2L],
      widths = rep(1 / ar, layout[1L]),
      heights = rep(1, layout[2L])
    )
  )

  for (i in seq_along(euler_grob$children)) {
    if (NCOL(pos) == 2L) {
      j <- pos[i, 1L]
      k <- pos[i, 2L]
    } else {
      j <- 1L
      k <- pos[i]
    }
    euler_grob$children[[i]]$vp <- grid::viewport(
      layout.pos.row = j,
      layout.pos.col = k,
      xscale = if (xlim[1] == -Inf) c(-1, 1) else xlim,
      yscale = if (ylim[1] == -Inf) c(-1, 1) else ylim,
      name = paste0("panel.vp.", j, ".", k)
    )
  }

  euler_grob$vp <- canvas_vp

  # return a gTree object
  children <- gList(
    if (do_bg) bg_grob = bg_grob,
    if (do_main) main_grob = main_grob,
    if (do_strip_top) strip_top_grob = strip_top_grob,
    if (do_strip_left) strip_left_grob = strip_left_grob,
    if (do_legend) legend_grob = legend_grob,
    euler_grob = euler_grob
  )

  grid::gTree(
    data = data,
    children = children,
    vp = grid::viewport(
      layout = grid::grid.layout(
        nrow = nrow,
        ncol = ncol,
        widths = widths,
        heights = heights,
        respect = TRUE
      ),
      name = "euler.vp"
    ),
    cl = "eulergram",
    name = "euler.diagram"
  )
}

resolve_strip_labels <- function(labels, levels, display_levels, axis) {
  if (is.null(labels)) {
    return(display_levels)
  }

  label_names <- names(labels)
  has_names <- !is.null(label_names) && any(nzchar(label_names))

  if (has_names) {
    if (any(!nzchar(label_names))) {
      stop(sprintf(
        "`strips$labels$%s` must be fully named when names are used.",
        axis
      ))
    }
    if (anyDuplicated(label_names)) {
      stop(sprintf("`strips$labels$%s` must have unique names.", axis))
    }

    unknown_levels <- setdiff(label_names, levels)
    if (length(unknown_levels) > 0L) {
      stop(sprintf("`strips$labels$%s` has unknown level names.", axis))
    }

    missing_levels <- setdiff(levels, label_names)
    if (length(missing_levels) > 0L) {
      stop(sprintf(
        "`strips$labels$%s` must provide labels for all levels.",
        axis
      ))
    }

    labels <- labels[match(display_levels, label_names)]
    names(labels) <- NULL
    return(labels)
  }

  if (length(labels) != length(display_levels)) {
    stop(
      sprintf(
        "`strips$labels$%s` must have length %d.",
        axis,
        length(display_levels)
      )
    )
  }

  labels
}

#' @rdname plot.euler
#' @export
plot.eulerr_venn <- function(
  x,
  fills = TRUE,
  patterns = FALSE,
  edges = TRUE,
  legend = FALSE,
  labels = identical(legend, FALSE),
  quantities = TRUE,
  strips = NULL,
  bg = FALSE,
  main = NULL,
  complement = TRUE,
  n = 200L,
  adjust_labels = TRUE,
  ...
) {
  if (!missing(adjust_labels)) {
    warning("`adjust_labels` is deprecated and no longer has any effect.")
  }

  plot.euler(
    x = x,
    fills = fills,
    patterns = patterns,
    edges = edges,
    legend = legend,
    labels = labels,
    quantities = quantities,
    strips = strips,
    bg = bg,
    main = main,
    complement = complement,
    n = n,
    ...
  )
}

#' @rdname plot.euler
#' @export
plot.venn <- function(...) {
  warning(
    "`plot.venn()` is deprecated and will be removed in a future release. ",
    "Use `plot()` on objects from `venn()` instead."
  )
  plot.eulerr_venn(...)
}

#' Print (plot) Euler diagram
#'
#' This function is responsible for the actual drawing of
#' `'eulergram'` objects created through [plot.euler()]. [print.eulergram()]
#' is an alias for [plot.eulergram()], which has been provided so that
#' [plot.euler()] gets called automatically.
#'
#' @param x an object of class `'eulergram'`, usually the output of
#'   [plot.euler()]
#' @param newpage if `TRUE`, opens a new page via [grid.newpage()] to draw on
#' @param ... ignored
#' @return A plot is drawn on the current device using [grid::Grid()] graphics.
#' @export
plot.eulergram <- function(x, newpage = TRUE, ...) {
  if (isTRUE(newpage)) {
    grid::grid.newpage()
  }
  grid::grid.draw(x)
}

#' @rdname plot.eulergram
#' @export
print.eulergram <- function(x, ...) {
  graphics::plot(x, ...)
}
