#' Standardized gtable Representation
#'
#' @description
#' The standardized gtable representation ensures that all plots share a
#' consistent row–column structure, facilitating flexible composition,
#' patch insertion, and alignment across multiple plots.
#'
#' This layout provides a fixed-size gtable grid for predictable positioning
#' of plot components such as titles, axes, legends, captions, and margins.
#' It is used internally for layout normalization and patch-based alignment.
#'
#' @usage NULL
#' @section Row structure (top -> bottom):
#' | Index | Component | Description |
#' |:------|:-----------|:-------------|
#' | 1 | `margin-top` | External top spacing |
#' | 2 | `tag-top` | Top tag (e.g., "A", "B") |
#' | 3 | `title` | Main title |
#' | 4 | `subtitle` | Subtitle |
#' | 5 | `guide-box-top` | Legend box at top |
#' | 6 | `guide-box-spacing` | Space between legend and main area |
#' | 7 | `patch-title-top`| Top patch title |
#' | 8 | `xlab-top` | Top x-axis label (rare) |
#' | 9 | `axis-top` | Top axis ticks and labels |
#' | 10 | `strip-top` | Top strip (facet label) |
#' | 11 | `panel` | Main plotting panel |
#' | 12 | `strip-bottom` | Bottom strip (facet label) |
#' | 13 | `axis-bottom` | Bottom axis ticks and labels |
#' | 14 | `xlab-bottom` | Bottom x-axis label |
#' | 15 | `patch-title-bottom`| Top patch title |
#' | 16 | `guide-box-spacing` | Space before bottom legend box |
#' | 17 | `guide-box-bottom` | Bottom legend box |
#' | 18 | `caption` | Caption or footnote text |
#' | 19 | `tag-bottom` | Bottom tag (optional) |
#' | 20 | `margin-bottom` | External bottom spacing |
#'
#' @section Column structure (left -> right):
#' | Index | Component | Description |
#' |:------|:-----------|:-------------|
#' | 1 | `margin-left` | External left spacing |
#' | 2 | `tag-left` | Optional side tag |
#' | 3 | `guide-box-left` | Left legend box |
#' | 4 | `guide-box-spacing` | Space between legend and panel |
#' | 5 | `patch-title-left`| Left patch title |
#' | 6 | `ylab-left` | Left y-axis label |
#' | 7 | `axis-left` | Left axis ticks and labels |
#' | 8 | `strip-left` | Left strip (facet label) |
#' | 9 | `panel` | Main panel area |
#' | 10 | `strip-right` | Right strip (facet label) |
#' | 11 | `axis-right` | Right axis ticks and labels |
#' | 12 | `ylab-right` | Right y-axis label |
#' | 13 | `patch-title-right`| Right patch title |
#' | 14 | `guide-box-spacing` | Space before right legend box |
#' | 15 | `guide-box-right` | Right legend box |
#' | 16 | `tag-right` | Optional tag on right side |
#' | 17 | `margin-right` | External right spacing |
#'
#' @name standardized_gtable
#' @keywords internal
NULL

TABLE_ROWS <- 18L + 2L
TABLE_COLS <- 15L + 2L

TOP_BORDER <- 9L + 1L
LEFT_BORDER <- 7L + 1L
BOTTOM_BORDER <- 8L + 1L
RIGHT_BORDER <- 7L + 1L

.TLBR <- c("top", "left", "bottom", "right")
.tlbr <- c("t", "l", "b", "r")

# position is a single string contains `.tlbr`
setdiff_position <- function(x, y) gsub(sprintf("[%s]", y), "", x)
union_position <- function(x, y) paste0(x, gsub(sprintf("[%s]", x), "", y))
split_position <- function(x) {
    vec_unique(.subset2(strsplit(x, "", fixed = TRUE), 1L))
}

setup_position <- function(x) setup_guides(x)

setup_guides <- function(x) {
    out <- .subset(
        c(t = "top", l = "left", b = "bottom", r = "right", i = "inside"),
        split_position(x)
    )
    names(out) <- NULL
    out
}

# pos is an atomic character
opposite_pos <- function(pos) {
    out <- .subset(
        c(top = "bottom", left = "right", bottom = "top", right = "left"),
        pos
    )
    names(out) <- NULL
    out
}

#' @importFrom ggplot2 zeroGrob
#' @importFrom gtable gtable gtable_add_grob
#' @importFrom grid unit
make_patch_table <- function() {
    widths <- unit(rep(0L, TABLE_COLS), "mm")
    widths[LEFT_BORDER + 1L] <- unit(1L, "null")
    heights <- unit(rep(0L, TABLE_ROWS), "mm")
    heights[TOP_BORDER + 1L] <- unit(1L, "null")
    ans <- gtable(widths, heights)
    gtable_add_grob(ans,
        list(zeroGrob()), TOP_BORDER + 1L, LEFT_BORDER + 1L,
        z = -Inf, name = "panel-area"
    )
}

#' Get Patch representation
#'
#' @description
#' `r code_quote(sprintf("%s::Patch", pkg_nm()), quote = FALSE)` represents the
#' layout manager for a single subplot within a composite plot. The `Patch`
#' object provides the interface for aligning the subplot, managing panel sizes,
#' and handling guide legends.
#'
#' @param x Any objects has a [Patch] representation
#' @return A `r code_quote(sprintf("%s::Patch", pkg_nm()), quote = FALSE)`
#' object.
#' @examples
#' patch(ggplot())
#' @seealso [`Patch`]
#' @export
#' @keywords internal
patch <- function(x) UseMethod("patch")

#' @export
patch.default <- function(x) {
    cli_abort(paste(
        "Each plot to be aligned must implement an {.fn patch}",
        "method. Object of {.obj_type_friendly {x}} does not."
    ))
}

#' @export
patch.NULL <- function(x) NULL

#' Options for Patch
#'
#' This class defines the options used by the `Patch` object. It includes:
#'
#' - `theme`: The theme to be applied, which can be either `NULL` or a ggplot2
#'   [theme][ggplot2::theme] object.
#' - `guides`: The guides for the plot, which can be `NULL` or a character
#'   vector.
#' - `tag`: Can be `NULL` (no tag), a single string, or a `LayoutTagger` object
#'   that provides a `$tag()` method to generate a tag string for each plot.
#'   The `LayoutTagger` is used specifically by [`alignpatches()`]. For
#'   individual plots, you typically call the `$tag()` method of the
#'   `LayoutTagger` object to return a string, which triggers the internal
#'   `Patch$tag()` method to add a tag.
#'
#' @keywords internal
patch_options <- S7::new_class("patch_options",
    properties = list(
        theme = S7::new_union(NULL, ggplot2::class_theme),
        guides = S7::new_union(NULL, S7::class_character),
        tag = S7::new_property(
            S7::new_union(
                NULL, S7::class_character,
                S7::new_S3_class("ggalign::LayoutTagger")
            ),
            validator = function(value) {
                if (is.character(value) && length(value) != 1L) {
                    return("must be a single string or `LayoutTagger` object")
                }
            }
        )
    )
)

#' Patch object
#'
#' @usage NULL
#' @format NULL
#' @details NULL
#' @description
#' In `alignpatches()`, each subplot require a `Patch` object for proper
#' alignment and layout operations. `Patch` object is a
#' [`ggproto()`][ggplot2::ggproto] object that provides the core methods for
#' arranging and aligning the plots.
#'
#' @importFrom ggplot2 ggproto find_panel
#' @importFrom grid unit unit.c grobWidth grobHeight
#' @importFrom gtable is.gtable
#' @export
#' @name Patch-ggproto
Patch <- ggproto(
    "ggalign::Patch", NULL,

    # @field plot
    #
    # The input subplot, stored in the `plot` field by convention.
    plot = NULL,

    #' @field options
    #'
    #' A [`patch_options`] object used to align the plot.
    options = NULL,

    #' @field set_options
    #'
    #' **Description**
    #'
    #' Sets multiple options at once in the `options` field. This method allows
    #' you to provide one or more key-value pairs, where each key represents an
    #' option name and its corresponding value is assigned to that option.
    #'
    #' **Arguments**
    #' - `...`: Key-value pairs to set specific properties of the `options`
    #'   object. Each argument should be of the form `name = value`, where
    #'   `name` is the option name and `value` is the value to assign to it.
    #'
    #' @importFrom rlang list2
    set_options = function(self, ...) props(self$options) <- list2(...),

    #' @field set_option
    #'
    #' **Description**
    #'
    #' Sets a specific option in the `options` field. This method allows you to
    #' modify individual properties of the `options` object by providing a
    #' key-value pair.
    #'
    #' **Arguments**
    #' - `name`: The name of the option to be set (as a character string).
    #' - `value`: The value to assign to the specified option.
    #' - `check`: Logical, if `TRUE` (default), checks the validity of the
    #'   option before assigning it. Set to `FALSE` to bypass the check.
    #'
    set_option = function(self, name, value, check = TRUE) {
        prop(self$options, name, check = check) <- value
    },

    #' @field get_option
    #' **Description**
    #'
    #' Retrieves a specific option from the `options` field.
    #'
    #' **Arguments**
    #' - `option`: The name of the option to retrieve.
    #'
    #' **Value**
    #' The value of the specified option.
    get_option = function(self, option) prop(self$options, option),

    #' @field setup
    #' **Description**
    #'
    #' (Optional method) Sets up the `options` and `data` for the plot.
    #'
    #' **Arguments**
    #' - `options`: A [`patch_options`] object that contains various layout
    #'   options.
    setup = function(self, options = NULL) {
        options <- options %||% patch_options()
        if (is_tagger(tagger <- prop(options, "tag"))) {
            prop(options, "tag", check = FALSE) <- tagger$tag()
        }
        self$options <- options
    },

    #' @field gtable
    #'
    #' **Description**
    #'
    #' (Required method) Constructs a
    #' [`standardized gtable`][standardized_gtable] object.
    #'
    #' **Value**
    #' A standardized [`gtable`][gtable::gtable] object, or a simple
    #' [`grob`][grid::grob] object.
    #'
    #' The returned object represents the graphical structure, which can either
    #' be a full table-based layout (`gtable`) for more complex arrangements or
    #' a simpler graphical object (`grob`) when only basic plot elements are
    #' involved.
    gtable = function(self) {
        cli_abort("{.fn gtable} method is not defined")
    },

    #' @field decompose_guides
    #' **Description**
    #'
    #' (Optional method) Collects guide legends and optionally removes the space
    #' they occupy.
    #'
    #' This method extracts guide legends based on the sides specified in the
    #' `guides` argument. After collecting the guides, the corresponding space
    #' in the `gt` is removed to free up space, except for guides placed
    #' `inside` the panel.
    #'
    #' **Arguments**
    #' - `gt`: A [`gtable`][gtable::gtable] object, usually returned by
    #'   `self$gtable()`.
    #'
    #' **Value**
    #' A list with:
    #' - `gt`: The updated gtable with guide legends removed (if applicable).
    #' - `guides`: A named list of collected guide grobs corresponding to the
    #'   sides specified in `guides` (or `NULL` if absent).
    decompose_guides = function(self, gt) {
        # By default we only consider gtable has guide legend box
        if (is.null(guides <- self$get_option("guides")) || !is.gtable(gt)) {
            return(list(gt = gt, guides = list()))
        }

        layout <- .subset2(gt, "layout")
        grobs <- .subset2(gt, "grobs")
        guides_ind <- grep("guide-box", .subset2(layout, "name"))

        # no guide legends to be collected
        if (length(guides_ind) == 0L) return(list()) # styler: off

        # collect guide legends from the `gt`
        guides_loc <- vec_slice(layout, guides_ind)
        collected_guides <- vector("list", length(guides))
        names(collected_guides) <- guides
        panel_loc <- find_panel(gt)
        removed <- NULL # guide legend grobs which will be removed
        for (guide_pos in guides) {
            guide_ind <- switch(guide_pos,
                top = .subset2(guides_loc, "b") < .subset2(panel_loc, "t"),
                left = .subset2(guides_loc, "r") < .subset2(panel_loc, "l"),
                bottom = .subset2(guides_loc, "t") > .subset2(panel_loc, "b"),
                right = .subset2(guides_loc, "l") > .subset2(panel_loc, "r"),
                inside = .subset2(guides_loc, "t") >= .subset2(panel_loc, "t") &
                    .subset2(guides_loc, "b") <= .subset2(panel_loc, "b") &
                    .subset2(guides_loc, "l") >= .subset2(panel_loc, "l") &
                    .subset2(guides_loc, "r") <= .subset2(panel_loc, "r")
            )
            if (!any(guide_ind)) next
            guide_loc <- vec_slice(guides_loc, guide_ind)
            guide_ind <- .subset(guides_ind, guide_ind)
            removed <- c(guide_ind, removed)
            collected_guides[[guide_pos]] <- .subset2(grobs, guide_ind)

            # remove the guide spaces from the original gtable
            # for inside guide, no need to remove the spaces
            if (guide_pos == "inside") next

            space_pos <- switch(guide_pos,
                top = ,
                left = 1L,
                bottom = ,
                right = -1L
            )
            if (guide_pos %in% c("right", "left")) {
                gt$widths[c(guide_loc$l, guide_loc$l + space_pos)] <- unit(
                    c(0L, 0L), "mm"
                )
            } else if (guide_pos %in% c("bottom", "top")) {
                gt$heights[c(guide_loc$t, guide_loc$t + space_pos)] <- unit(
                    c(0L, 0L), "mm"
                )
            }
        }
        if (length(removed)) gt <- subset_gt(gt, -removed, trim = FALSE)
        list(gt = gt, guides = collected_guides)
    },

    #' @field tag
    #'
    #' Add a Tag to a `gtable`
    #'
    #' **Description**:
    #' This function adds a tag to a given `gtable` at a specified position. The
    #' tag is added only if the `gtable` is standardized (i.e., it has the
    #' expected number of rows and columns)..
    #'
    #' **Arguments**:
    #' - `gt`: A `gtable` object to which the tag will be added.
    #' - `label`: A string representing the label or content for the tag.
    #' - `t`, `l`, `b`, `r`: Numeric values representing the top, left, bottom,
    #'   and right margins (coordinates) for placing the tag on the `gtable`.
    #' - `z`: A numeric value representing the z-order, controlling the stacking
    #'   order of the tag relative to other elements in the `gtable`.
    #'
    #' **Value**:
    #' The modified `gtable` with the tag added. If the `gtable` is not
    #' standardized (i.e., it doesn't meet the row/column requirements), the
    #' function returns the original `gtable` without modification.
    tag = function(self, gt, label, t, l, b, r, z) {
        if (!is.gtable(gt)) {
            return(gt)
        }
        # Note: When the gtable represents a facetted plot, the number of
        #   rows/columns (heights or widths) will exceed TABLE_ROWS/COLS.
        if (nrow(gt) < TABLE_ROWS || ncol(gt) < TABLE_COLS) {
            return(gt)
        }
        table_add_tag(gt, label, self$get_option("theme"), t, l, b, r, z)
    },
    #' @field panel_sizes
    #' **Description**
    #'
    #' This method retrieves the panel sizes (widths and heights) of a `gtable`
    #' or `grob` object.
    #'
    #' **Arguments**
    #' - `gt`: A [`gtable`][gtable::gtable] object, usually returned by
    #'   `self$decompose_guides()`.
    #'
    #' **Value**
    #' A list with components:
    #' - `widths`: The panel widths as a unit object
    #' - `heights`: The panel heights as a unit object
    #'
    #' @importFrom grid grobHeight grobWidth
    #' @importFrom gtable is.gtable
    #' @importFrom ggplot2 find_panel
    panel_sizes = function(self, gt) {
        if (is.gtable(gt)) {
            panel_pos <- find_panel(gt)
            if (nrow(panel_pos) == 0L) {
                return(NULL)
            }
            list(
                widths = .subset2(gt, "widths")[
                    .subset2(panel_pos, "l"):.subset2(panel_pos, "r")
                ],
                heights = .subset2(gt, "heights")[
                    .subset2(panel_pos, "t"):.subset2(panel_pos, "b")
                ]
            )
        } else if (is.grob(gt)) {
            list(widths = grobWidth(gt), heights = grobHeight(gt))
        } else {
            NULL
        }
    },

    #' @field adjust_panel_width/adjust_panel_height
    #' **Description**
    #'
    #' (Optional method) In most cases, the panel sizes do not need to be
    #' manually adjusted when aligning, as long as their border sizes are
    #' consistent. However, for [`gtable`][gtable::gtable] objects with absolute
    #' panel sizes, the panel sizes must be directly set to ensure that all plot
    #' panels fill in. This is particularly important when the specified and
    #' desired panel sizes are in absolute units. The method is only called
    #' when the plot does not span across multiple areas.
    #'
    #' **Arguments**
    #' - `panel_widths`/`panel_heights`: Unit objects representing the panel
    #'   widths or heights of the underlying [`gtable`][gtable::gtable] object.
    #'   These values correspond to the `widths`/`heights` fields returned by
    #'   the `$panel_sizes()` method.
    #' - `panel_width`/`panel_height`: Unit objects representing the desired
    #'   panel size. If the internal numeric value is `NA`, the size will be
    #'   computed based on `panel_widths`/`panel_heights`.
    #'
    #' **Value**
    #' The panel width/height as a unit object
    #'
    #' @importFrom grid is.unit
    adjust_panel_width = function(self, panel_widths, panel_width) {
        if (is.unit(panel_widths) && all(is_absolute_unit(panel_widths))) {
            if (is.na(as.numeric(panel_width))) {
                panel_width <- convertWidth(sum(panel_widths), "mm")
            } else if (is_absolute_unit(panel_width)) {
                panel_width <- max(panel_width, sum(panel_widths))
                panel_width <- convertWidth(panel_width, "mm")
            }
        }
        panel_width
    },
    adjust_panel_height = function(self, panel_heights, panel_height) {
        if (is.unit(panel_heights) && all(is_absolute_unit(panel_heights))) {
            if (is.na(as.numeric(panel_height))) {
                panel_height <- convertHeight(sum(panel_heights), "mm")
            } else if (is_absolute_unit(panel_height)) {
                panel_height <- max(panel_height, sum(panel_heights))
                panel_height <- convertHeight(panel_height, "mm")
            }
        }
        panel_height
    },

    #' @field respect_panel
    #' **Description**
    #'
    #' (Optional method) In most cases, panel sizes do not need to be manually
    #' adjusted when aligning panels, as long as their border sizes are
    #' consistent. However, for `gtables` with a fixed panel aspect ratio, if
    #' the internal *numeric value* of the input panel sizes (either
    #' `panel_width` or `panel_height` is `NA`), that dimension will be inferred
    #' while maintaining the aspect ratio, particularly for single-panel
    #' layouts.
    #'
    #' **Arguments**
    #' - `gt`: A [`gtable`][gtable::gtable] object, usually returned by
    #'   `self$decompose_guides()`.
    #' - `panel_width`/`panel_height`: Unit objects specifying the desired panel
    #'   size. If the internal numeric value of either is `NA`, the size is
    #'   computed from the gtable (`gt`).
    #'
    #' **Value**
    #' A list with components:
    #' - `width`: The panel width as a unit object
    #' - `height`: The panel height as a unit object
    #' - `respect`: If `TRUE`, the aspect ratio was enforced
    #'
    #' @importFrom ggplot2 find_panel
    #' @importFrom gtable is.gtable
    respect_panel = function(self, gt, panel_width, panel_height) {
        # By default we only consider gtable has panel area
        if (!is.gtable(gt)) {
            return(list(width = panel_width, height = panel_height))
        }

        # Locate the panel within the gtable
        panel_pos <- find_panel(gt)
        row <- .subset2(panel_pos, "t")
        col <- .subset2(panel_pos, "l")

        # try to maintain aspect ratio if there is a single facet panel
        if (row == .subset2(panel_pos, "b") &&
            col == .subset2(panel_pos, "r")) {
            respect <- is_respect(gt)
            # Continue only if 'respect' is enabled
            if (respect) {
                # Determine which dimension(s) can be inferred
                can_set_width <- is.na(as.numeric(panel_width))
                can_set_height <- is.na(as.numeric(panel_height))

                # Extract intrinsic panel dimensions from the gtable
                w <- .subset2(gt, "widths")[col]
                h <- .subset2(gt, "heights")[row]

                # Infer dimensions while maintaining aspect ratio
                if (can_set_width && can_set_height) {
                    panel_width <- w
                    panel_height <- h
                } else if (can_set_width) {
                    panel_width <- as.numeric(w) / as.numeric(h) * panel_height
                } else if (can_set_height) {
                    panel_height <- as.numeric(h) / as.numeric(w) * panel_width
                } else {
                    # Both dimensions are already set, no need to adjust
                    respect <- FALSE
                }
            }
        } else {
            respect <- FALSE
        }
        list(
            width = panel_width, height = panel_height,
            # guess_width = guess_width, guess_height = guess_height,
            respect = respect
        )
    },

    #' @field border_sizes
    #' **Description**
    #'
    #' (Optional method) retrieve the border sizes of a gtable.
    #'
    #' **Arguments**
    #' - `gt`: A [`gtable`][gtable::gtable] object, usually returned by
    #'   `self$decompose_guides()`.
    #'
    #' **Value**
    #' A list with components:
    #' - `top`: `unit` values for the top borders or `NULL`.
    #' - `left`: `unit` values for the left borders or `NULL`.
    #' - `bottom`: `unit` values for the bottom borders or `NULL`.
    #' - `right`: `unit` values for the right borders or `NULL`.
    #'
    #' @importFrom gtable is.gtable
    #' @importFrom ggplot2 find_panel
    border_sizes = function(self, gt) {
        if (!is.gtable(gt)) {
            return(NULL)
        }
        heights <- .subset2(gt, "heights")
        widths <- .subset2(gt, "widths")
        panel_pos <- find_panel(gt)
        if (.subset2(panel_pos, "t") != TOP_BORDER + 1L) {
            top <- NULL
        } else {
            top <- heights[seq_len(TOP_BORDER)]
        }
        if (.subset2(panel_pos, "l") != LEFT_BORDER + 1L) {
            left <- NULL
        } else {
            left <- widths[seq_len(LEFT_BORDER)]
        }
        if (.subset2(panel_pos, "b") != length(heights) - BOTTOM_BORDER) {
            bottom <- NULL
        } else {
            bottom <- heights[
                (length(heights) - BOTTOM_BORDER + 1L):length(heights)
            ]
        }
        if (.subset2(panel_pos, "r") != length(widths) - RIGHT_BORDER) {
            right <- NULL
        } else {
            right <- widths[(length(widths) - RIGHT_BORDER + 1L):length(widths)]
        }
        list(top = top, left = left, bottom = bottom, right = right)
    },

    #' @field align_border
    #' **Description**
    #'
    #' (Optional method) This method modifies the top, left, bottom, and right
    #' border sizes of the underlying gtable (`gt`) by replacing corresponding
    #' entries in its `heights` and `widths` vectors..
    #'
    #' **Arguments**
    #' - `gt`: A [`gtable`][gtable::gtable] object, usually returned by
    #'   `self$decompose_guides()`.
    #' - `t`, `l`, `b`, `r`: Optional numeric vectors specifying new sizes for
    #'   the top, left, bottom, and right borders, respectively. Each vector
    #'   replaces the corresponding entries in `gt$heights` or `gt$widths`.
    #'
    #' **Value**
    #' A modified [`gtable`][gtable::gtable] object.
    #' @importFrom gtable is.gtable
    align_border = function(self, gt, t, l, b, r) {
        if (!is.gtable(gt)) return(gt) # styler: off
        if (!is.null(t)) gt$heights[seq_along(t)] <- t
        if (!is.null(l)) gt$widths[seq_along(l)] <- l
        if (!is.null(b)) {
            n_row <- nrow(gt)
            gt$heights[seq(n_row - length(b) + 1L, n_row)] <- b
        }
        if (!is.null(r)) {
            n_col <- ncol(gt)
            gt$widths[seq(n_col - length(r) + 1L, n_col)] <- r
        }
        gt
    },

    #' @field place
    #'
    #' **Description**
    #' (Optional method) Inserts the patch's gtable (including optional
    #' background) into the target canvas gtable.
    #'
    #' This method places the patch's gtable into a specified location of
    #' another gtable, preserving the background and plot layers separately if a
    #' background exists. The `t`, `l`, `b`, `r` arguments specify the position
    #' in the target gtable, and `bg_z` / `plot_z` define the stacking order
    #' (z-order) for background and plot.
    #'
    #' **Arguments**
    #' - `gtable`: the target canvas gtable into which the patch will be
    #'   inserted.
    #' - `gt`: A [`gtable`][gtable::gtable] object, usually returned by
    #'   `self$align_border()`.
    #' - `t`, `l`, `b`, `r`: Integer positions (top, left, bottom, right)
    #'   specifying where to insert the patch in the target gtable.
    #' - `i`: Index of the current patch, used to generate unique grob names.
    #' - `bg_z`: Z-order for the background grob (default `1L`).
    #' - `plot_z`: Z-order for the plot grob (default `2L`).
    #' - `options`: A [`patch_options`] object containing various layout
    #'   options. Typically, this is the value returned by the subplot's
    #'   `self$setup()` method.
    #'
    #' **Details**
    #' - If the patch includes a grob named `"background"`, it is separated from
    #'   the main plot and inserted independently from the plot grob.
    #' - If no background is present, the entire gtable is inserted as the plot
    #'   grob.
    #'
    #' **Value**
    #' The modified target canvas gtable with the patch's gtable added.
    place = function(self, gtable, gt, t, l, b, r, i, bg_z, plot_z) {
        if (is.gtable(gt)) {
            components <- self$decompose_bg(gt)
            if (!is.null(.subset2(components, "bg"))) {
                gtable <- self$place_bg(
                    gtable, .subset2(components, "bg"),
                    t, l, b, r, i, bg_z
                )
            }
            gt <- .subset2(components, "gt")
        }
        self$place_gt(gtable, gt, t, l, b, r, i, plot_z)
    },

    #' @field decompose_bg
    #'
    #' **Description**
    #' Separates the background grob (if present) from the main gtable.
    #'
    #' **Value**
    #' A list with:
    #' - `bg`: The background grob (or `NULL` if absent)
    #' - `gt`: The gtable with background removed
    decompose_bg = function(self, gt) {
        layout <- .subset2(gt, "layout")
        background <- layout$name == "background"
        if (!any(background)) {
            return(list(bg = NULL, gt = gt))
        }
        bg <- .subset(.subset2(gt, "grobs"), background)
        gt <- subset_gt(gt, !background, trim = FALSE)
        list(bg = bg, gt = gt)
    },

    #' @field place_bg
    #'
    #' **Description**
    #' Adds the background grob into the target gtable.
    place_bg = function(self, gtable, bg, t, l, b, r, i, z) {
        gtable_add_grob(
            gtable,
            grobs = bg,
            t = t, l = l, b = b, r = r,
            name = paste("plot", i, "background", sep = "-"),
            z = z
        )
    },

    #' @field place_gt
    #'
    #' **Description**
    #' Adds the main plot gtable into the target gtable.
    place_gt = function(self, gtable, gt, t, l, b, r, i, z) {
        gtable_add_grob(
            gtable,
            grobs = gt,
            t = t, l = l, b = b, r = r,
            name = paste("plot", i, sep = "-"),
            z = z
        )
    },

    #' @field is_alignpatches
    #'
    #' **Description**
    #'
    #' Checks whether the object inherits from the [alignpatches()] `Patch`
    #' representation.
    #'
    #' If `TRUE`, the `self$alignpatches` will include `patches`, `gt_list`,
    #' and `borders_list`. For an example of how these are used, refer to the
    #' `patch.ggalign_free_lab` function in the `alignpatch-free-lab.R` script.
    #'
    #' **Value**
    #' Logical value (`TRUE` or `FALSE`) indicating whether `self` is a
    #' `PatchAlignpatches` object.
    is_alignpatches = function(self) inherits(self, "PatchAlignpatches")
)
