#' Compute Any Possible Table
#'
#' @description
#' [any_table()] produces any possible descriptive table in 'Excel' format. Any number
#' of variables can be nested and crossed. The output is an individually styled
#' 'Excel' table, which also receives named ranges, making it easier to read the data back in.
#'
#' @param data_frame A data frame in which are the variables to tabulate.
#' @param rows A vector that provides single variables or variable combinations that
#' should appear in the table rows. To nest variables use the form:
#' "var1 + var2 + var3 + ...".
#' @param columns A vector that provides single variables or variable combinations that
#' should appear in the table rows. To nest variables use the form:
#' "var1 + var2 + var3 + ...".
#' @param values A vector containing all variables that should be summarised.
#' @param statistics Available functions:
#' - "sum"       -> Weighted and unweighted sum
#' - "sum_wgt"   -> Sum of all weights
#' - "freq"      -> Unweighted frequency
#' - "freq_g0"   -> Unweighted frequency of all values greater than zero
#' - "pct_group" -> Weighted and unweighted percentages within the respective group
#' - "pct_value" -> Weighted and unweighted percentages between value variables or of a variable expression
#' - "pct_total" -> Weighted and unweighted percentages compared to the grand total
#' - "mean"      -> Weighted and unweighted mean
#' - "median"    -> Weighted and unweighted median
#' - "mode"      -> Weighted and unweighted mode
#' - "min"       -> Minimum
#' - "max"       -> Maximum
#' - "sd"        -> Weighted and unweighted standard deviation
#' - "variance"  -> Weighted and unweighted standard variance
#' - "first"     -> First value
#' - "last"      -> Last value
#' - "pn"        -> Weighted and unweighted percentiles (any p1, p2, p3, ... possible)
#' Uses the SAS default way of calculating percentiles, which is Type 2 described in [quantile()].
#' - "missing"   -> Missings generated by the value variables
#' @param pct_group This option is used to determine which variable of the row and column
#' variables should add up to 100 %. Multiple variables can be specified in a vector to
#' generate multiple group percentages. You can also use the keywords "row_pct" or "col_pct"
#' to calculate total percentages for rows and columns regardless of the respective other dimension.
#' @param pct_value You can pass a list here, which contains the information for a new
#' variable name and between which of the value variables percentages should be computed.
#' @param pct_block Can be "rows" or "columns". Calculates percentages for the last
#' group of variables inside the individual row or column combinations.
#' @param compute A list in which individual calculations are handled. For the writing style
#' see the [compute.()] function.
#' @param formats A list in which is specified which formats should be applied to which variables.
#' @param by Compute tables stratified by the expressions of the provided variables.
#' @param weight Put in a weight variable to compute weighted results.
#' @param order_by Determine how the columns will be ordered. "values" orders the results by the
#' order you provide the variables in values. "stats" orders them by the order under statistics.
#' "values_stats" is a combination of both. "columns" keeps the order as given in columns
#' and "interleaved" alternates the stats.
#' @param titles Specify one or more table titles.
#' @param footnotes Specify one or more table footnotes.
#' @param var_labels A list in which is specified which label should be printed for
#' which variable instead of the variable name.
#' @param stat_labels A list in which is specified which label should be printed for
#' which statistic instead of the statistic name.
#' @param box Provide a text for the upper left box of the table.
#' @param workbook Insert a previously created workbook to expand the sheets instead of
#' creating a new file.
#' @param style A list of options can be passed to control the appearance of 'Excel' outputs.
#' Styles can be created with [excel_output_style()].
#' @param output The following output formats are available: excel and excel_nostyle.
#' @param na.rm FALSE by default. If TRUE removes all NA values from the variables.
#' @param print_miss FALSE by default. If TRUE outputs all possible categories of the
#' grouping variables based on the provided formats, even if there are no observations
#' for a combination.
#' @param print TRUE by default. If TRUE prints the output, if FALSE doesn't print anything. Can be used
#' if one only wants to catch the output data frame and workbook with meta information.
#' @param monitor FALSE by default. If TRUE, outputs two charts to visualize the functions time consumption.
#'
#' @details
#' [any_table()] is based on the 'SAS' procedure Proc Tabulate, which provides
#' efficient and readable ways to perform complex tabulations.
#'
#' With this function you can combine any number of variables in any possible way, all
#' at once. You just define which variables or variable combinations should end up in
#' the table rows and columns with a simple syntax. Listing variables in a vector like
#' c("var1", "var2", "var3",...) means to put variables below (in case of the
#' row variables) or besides (in case of the column variables) each other. Nesting variables
#' is as easy as putting a plus sign between them, e.g. c("var1 + var2", "var2" + "var3" + "var4", etc.).
#' And of course you can combine both versions.
#'
#' The real highlight is, that this function not only creates all the desired variable
#' combinations and exports them to an 'Excel' file, it prints a fully custom styled
#' table to a workbook. Setting up a custom, reusable style is as easy as setting up
#' options like: provide a color for the table header, set the font size for the row header,
#' should borders be drawn for the table cells yes/no, and so on. Merging doubled header texts,
#' happens automatically.
#'
#' With this function you basically can fully concentrate on designing a table, instead of
#' thinking hard about how to calculate where to put a border or to even manually prepare
#' a designed workbook.
#'
#' @return
#' Returns a list with the data table containing the results for the table, the formatted
#' 'Excel' workbook and the meta information needed for styling the final table.
#'
#' @seealso
#' Creating a custom table style: [excel_output_style()], [modify_output_style()],
#' [number_format_style()], [modify_number_formats()].
#'
#' Global style options: [set_style_options()], [set_labels()].
#'
#' Other global options: [set_titles()], [set_footnotes()], [set_print()], [set_monitor()],
#' [set_na.rm()], [set_print()], [set_print_miss()], [set_output()].
#'
#' Combine Excel workbooks: [combine_into_workbook()].
#'
#' Creating formats: [discrete_format()] and [interval_format()].
#'
#' Functions that can handle formats and styles: [frequencies()], [crosstabs()].
#'
#' Additional functions that can handle styles: [export_with_style()]
#'
#' Additional functions that can handle formats: [summarise_plus()], [recode.()],
#' [recode_multi()], [transpose_plus()], [sort_plus()]
#'
#' @examples
#' # NOTE: Some of the examples use the parameter 'output = "excel_nostyle"' to
#' #       make them run faster so that more examples can be provided. Take out
#' #       the parameter to get the fully styled excel table.
#' #       The global option for printing the output is also set to FALSE for
#' #       the same reason. When running the examples manually, don't run this
#' #       option here.
#' set_print(FALSE)
#' set_style_options(background_color = "")
#'
#' # Example data frame
#' my_data <- dummy_data(1000)
#' my_data[["person"]] <- 1
#'
#' # Formats
#' age. <- discrete_format(
#'     "Total"          = 0:100,
#'     "under 18"       = 0:17,
#'     "18 to under 25" = 18:24,
#'     "25 to under 55" = 25:54,
#'     "55 to under 65" = 55:64,
#'     "65 and older"   = 65:100)
#'
#' sex. <- discrete_format(
#'     "Total"  = 1:2,
#'     "Male"   = 1,
#'     "Female" = 2)
#'
#' # NOTE: "!" in front of an expression makes the expression available for
#' #       calculations but prevents it from beeing printed out.
#' education. <- discrete_format(
#'     "!Total"           = c("low", "middle", "high"),
#'     "low education"    = "low",
#'     "middle education" = "middle",
#'     "high education"   = "high")
#'
#' state. <- discrete_format(
#'     "Germany"                       = 1:16,
#'     "Schleswig-Holstein"            = 1,
#'     "Hamburg"                       = 2,
#'     "Lower Saxony"                  = 3,
#'     "Bremen"                        = 4,
#'     "North Rhine-Westphalia"        = 5,
#'     "Hesse"                         = 6,
#'     "Rhineland-Palatinate"          = 7,
#'     "Baden-Württemberg"             = 8,
#'     "Bavaria"                       = 9,
#'     "Saarland"                      = 10,
#'     "West"                          = 1:10,
#'     "Berlin"                        = 11,
#'     "Brandenburg"                   = 12,
#'     "Mecklenburg-Western Pomerania" = 13,
#'     "Saxony"                        = 14,
#'     "Saxony-Anhalt"                 = 15,
#'     "Thuringia"                     = 16,
#'     "East"                          = 11:16)
#'
#' # Define style
#' set_style_options(column_widths = c(2, 15, 15, 15, 9))
#'
#' # Define titles and footnotes. If you want to add hyperlinks you can do so by
#' # adding "link:" followed by the hyperlink to the main text. Linking to another
#' # cell works with "cell:". To link to a file use "file:" an pass the full file
#' # path afterwards.
#' set_titles("This is title number 1",
#'            "This is title number 2 link: https://cran.r-project.org/",
#'            "This is title number 3 cell: W22",
#'            "This is title number 4 file: C:/MyFolder/MyFile.txt")
#'
#' set_footnotes("This is footnote number 1",
#'               "This is footnote number 2 file: C:/MyFolder/MyFile.txt",
#'               "This is footnote number 3 cell: W22",
#'               "This is footnote number 4 link: https://cran.r-project.org/")
#'
#' # Output complex tables with different percentages
#' my_data |> any_table(rows      = c("sex + age", "sex", "age"),
#'                      columns   = c("year", "education + year"),
#'                      values    = weight,
#'                      pct_group = c("row_pct", "col_pct"),
#'                      formats   = list(sex = sex., age = age.,
#'                                       education = education.),
#'                      na.rm     = TRUE)
#'
#' # If you want to get a clearer vision of what the result table looks like, in terms
#' # of the row and column categories, you can write the code like this, to make out
#' # the variable crossings and see the order.
#' # my_data |> any_table(columns   = c(            "year", "education + year"),
#' #                      rows      = c("sex + age",
#' #                                    "sex",
#' #                                    "age"),
#' #                      values    = weight,
#' #                      pct_group = c("sex", "age"),
#' #                      formats   = list(sex = sex., age = age.,
#' #                                       education = education.),
#' #                      output    = "excel_nostyle",
#' #                      na.rm     = TRUE)
#'
#' # Percentages based on value variables instead of categories
#' my_data |> any_table(rows       = c("age + year"),
#'                      columns    = "sex",
#'                      values     = c(probability, person),
#'                      statistics = c("pct_value", "sum", "freq"),
#'                      pct_value  = list(rate = "probability / person"),
#'                      weight     = weight,
#'                      formats    = list(sex = sex., age = age.),
#'                      output    = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # Percentages based on a formatted variable expression
#' my_data |> any_table(rows       = c("age + year"),
#'                      columns    = "sex",
#'                      values     = c(probability, person),
#'                      pct_value  = list(sex = "Total", age = "Total"),
#'                      weight     = weight,
#'                      formats    = list(sex = sex., age = age.),
#'                      output    = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # Percentages based on variable combination blocks
#' # NOTE: age + (year, education) becomes "age + year" and "age + education"
#' #       and will be sorted together. Also works in columns. You can also
#' #       do something like this: age + (year, sex + education, education).
#' my_data |> any_table(rows       = c("sex + (age, education)"),
#'                      columns    = "year",
#'                      values     = weight,
#'                      pct_block  = c("rows", "columns"),
#'                      formats    = list(sex = sex., age = age.,
#'                                        education = education.),
#'                      output     = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # Individual calculations
#' my_data |> any_table(rows       = c("age + year"),
#'                      columns    = "sex",
#'                      values     = "probability",
#'                      statistics = c("sum", "sum_wgt"),
#'                      compute    = list(percent    = probability_sum * 100 / sum_wgt,
#'                                        pct_square = percent ^ 2),
#'                      weight     = weight,
#'                      formats    = list(sex = sex., age = age.),
#'                      output    = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # Customize the visual appearance by adding variable and statistic labels. Both
#' # can also be set as a global option, if labels should be reused over multiple
#' # tables.
#' # Note: You don't have to describe every element. Sometimes a table can be more
#' # readable with less text. To completely remove a variable label just put in an
#' # empty text "" as label.#'
#' set_titles("This is title number 1",
#'            "This is title number 2",
#'            "This is title number 3")
#'
#' set_footnotes("This is footnote number 1",
#'               "This is footnote number 2",
#'               "This is footnote number 3")
#'
#' my_data |> any_table(rows        = c("age + year"),
#'                      columns     = "sex",
#'                      values      = weight,
#'                      statistics  = c("sum", "pct_group"),
#'                      order_by    = "interleaved",
#'                      formats     = list(sex = sex., age = age.),
#'                      var_labels  = list(age = "Age categories",
#'                                         sex = "", weight = ""),
#'                      stat_labels = list(pct = "%"),
#'                      output      = "excel_nostyle",
#'                      na.rm       = TRUE)
#'
#' # Individual styling can also be passed directly
#' my_style <- excel_output_style(header_back_color = "0077B6",
#'                                font              = "Times New Roman")
#'
#' # my_data |> any_table(rows       = c("age + year"),
#' #                      columns    = "sex",
#' #                      values     = c(probability, person),
#' #                      statistics = c("pct_value", "sum", "freq"),
#' #                      pct_value  = list(rate = "probability / person"),
#' #                      weight     = weight,
#' #                      formats    = list(sex = sex., age = age.),
#' #                      style      = my_style,
#' #                      na.rm      = TRUE)
#'
#' # Select specific statistics for specific variables
#' my_data |> any_table(rows       = "sex",
#'                      columns    = "year",
#'                      statistics = list("sum"       = c(weight, income),
#'                                        "pct_group" = balance),
#'                      formats    = list(sex = sex.),
#'                      output     = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # Pass on workbook to create more sheets in the same file
#' my_style <- my_style |> modify_output_style(sheet_name = "age_sex")
#'
#' result_list <- my_data |>
#'            any_table(rows       = "age",
#'                      columns    = "sex",
#'                      values     = weight,
#'                      statistics = "sum",
#'                      formats    = list(sex = sex., age = age.),
#'                      style      = my_style,
#'                      output     = "excel_nostyle",
#'                      na.rm      = TRUE,
#'                      print      = FALSE)
#'
#' my_style <- my_style |> modify_output_style(sheet_name = "edu_year")
#'
#' my_data |> any_table(workbook   = result_list[["workbook"]],
#'                      rows       = "education",
#'                      columns    = "year",
#'                      values     = weight,
#'                      statistics = "pct_group",
#'                      formats    = list(education = education.),
#'                      style      = my_style,
#'                      output     = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # In case you have a good amount of tables, you want to combine in a single workbook,
#' # you can also catch the outputs and combine them afterwards in one go.
#' # NOTE: This section is commented out to just show the principle. Otherwise the
#' #       example code would run for too long.
#' # set_style_options(sheet_name = "sheet1")
#' # tab1 <- my_data |> any_table(..., print = FALSE, output = "excel_nostyle")
#' #
#' # set_style_options(sheet_name = "sheet2")
#' # tab2 <- my_data |> any_table(..., print = FALSE, output = "excel_nostyle")
#' #
#' # set_style_options(sheet_name = "sheet3")
#' # tab3 <- my_data |> any_table(..., print = FALSE, output = "excel_nostyle")
#' #
#' # ...
#' #
#' # Every of the above tabs is a list, which contains the data table, an unstyled
#' # workbook and the meta information needed for the individual styling. These tabs
#' # can be input into the following function, which reads the meta information, styles
#' # each table individually and combines them as separate sheets into a single workbook.
#' # combine_into_workbook(tab1, tab2, tab3 file = "C:/My_folder/My_workbook.xlsx")
#'
#' # The result list from above also carries the transformed data frame if
#' # needed for further usage
#' any_table_df <- result_list[["table"]]
#'
#' # Output multiple complex tables by expressions of another variable.
#' # If you specify the sheet name as "by" in the output style, the sheet
#' # names are named by the variable expressions of the by-variable. Otherwise
#' # the given sheet named gets a running number.
#' my_style <- my_style |> modify_output_style(sheet_name = "by")
#'
#' my_data |> any_table(rows       = c("sex", "age"),
#'                      columns    = "education",
#'                      values     = weight,
#'                      by         = state,
#'                      statistics = c("sum", "pct_group"),
#'                      pct_group  = "education",
#'                      formats    = list(sex = sex., age = age., state = state.,
#'                                        education = education.),
#'                      output     = "excel_nostyle",
#'                      na.rm      = TRUE)
#'
#' # To save a table as xlsx file you have to set the path and filename in the
#' # style element
#' # Example files paths
#' table_file <- tempfile(fileext = ".xlsx")
#'
#' # Note: Normally you would directly input the path ("C:/MyPath/") and name ("MyFile.xlsx").
#' set_style_options(save_path  = dirname(table_file),
#'                   file       = basename(table_file),
#'                   sheet_name = "MyTable")
#'
#' my_data |> any_table(rows    = "sex",
#'                      columns = "year",
#'                      values  = weight,
#'                      formats = list(sex = sex.))
#'
#' # Manual cleanup for example
#' unlink(table_file)
#'
#' # Global options are permanently active until the current R session is closed.
#' # There are also functions to reset the values manually.
#' reset_style_options()
#' reset_qol_options()
#' close_file()
#'
#' set_print(TRUE)
#' set_style_options(background_color = "FFFFFF")
#'
#' @export
any_table <- function(data_frame,
                      rows,
                      columns        = "",
                      values,
                      statistics     = NULL,
                      pct_group      = c(),
                      pct_value      = list(),
                      pct_block      = c(),
                      compute        = list(),
                      formats        = list(),
                      by             = c(),
                      weight         = NULL,
                      order_by       = "stats",
                      titles         = .qol_options[["titles"]],
                      footnotes      = .qol_options[["footnotes"]],
                      var_labels     = .qol_options[["var_labels"]],
                      stat_labels    = .qol_options[["stat_labels"]],
                      box            = "",
                      workbook       = NULL,
                      style          = .qol_options[["excel_style"]],
                      output         = .qol_options[["output"]],
                      na.rm          = .qol_options[["na.rm"]],
                      print_miss     = .qol_options[["print_miss"]],
                      print          = .qol_options[["print"]],
                      monitor        = .qol_options[["monitor"]]){

    # Measure the time
    print_start_message()
	print_step("GREY", "Error handling")

    #-------------------------------------------------------------------------#
    monitor_df <- NULL |> monitor_start("Error handling", "Preparation")
    #-------------------------------------------------------------------------#

    ###########################################################################
    # Early evaluations
    ###########################################################################

    # First convert data frame to data table
    if (!data.table::is.data.table(data_frame)){
        data_frame <- data.table::as.data.table(data_frame)
    }

    # Evaluate formats early
    if (!is_list_of_dfs(formats)){
        formats_list <- as.list(substitute(formats))[-1]
        formats      <- evaluate_formats(formats_list)
    }

    # Remove empty formats and throw a warning. This can happen if there is e.g.
    # a typo in the format.
    for (variable in names(formats)){
        if (is.null(formats[[variable]])){
            formats[[variable]] <- NULL
            print_message("WARNING", "Format for variable '[variable]' does not exist and can't be applied.", variable = variable)
        }
    }

    # Apply macros to save path and file
    if (!is.null(style[["save_path"]])){
        style[["save_path"]] <- macro(style[["save_path"]])
    }

    if (!is.null(style[["file"]])){
        style[["file"]] <- macro(style[["file"]])
    }

    ###########################################################################
    # Error handling
    ###########################################################################

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Workbook
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Check whether the whole result list of one of the tabulation functions was passed
    if (!is.null(workbook) && is.list(workbook)){
        # If the workbook object is part of the list, extract it
        if ("workbook" %in% names(workbook)){
            workbook <- workbook[["workbook"]]
        }
        # If the list is some other list without workbook object, abort
        else{
            print_message("ERROR", c("Workbook object is invalid. You have to provide a workbook object",
                                     "created with one of the tabulation functions. Tabulation will be aborted."))
            return(invisible(NULL))
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Row variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Enable the use of macro variables
    rows <- apply_macro(rows)

    # Expand brackets first to be able to identify the unique variables used afterwards
    row_expand <- expand_brackets(rows)

    if (row_expand[[2]]){
        rows <- names(row_expand[[1]])
    }
    else{
        row_expand <- NULL
    }

    # Get row variables from provided combinations
    row_vars <- unlist_variables(rows)

    if (is.null(row_vars)){
        print_message("ERROR", "<Rows> variables must be provided in quotation marks. Tabulation will be aborted.")
        return(invisible(NULL))
    }

    row_vars <- data_frame |> part_of_df(row_vars, check_only = TRUE)

    if (is.list(row_vars)){
        print_message("ERROR", c("The provided <rows> variable[?s] '[vars]' [?is/are] not part of",
								 "the data frame. Tabulation will be aborted."), vars = row_vars[[1]])
        return(invisible(NULL))
    }

    if (length(rows) <= 1){
        if (length(rows) == 0 || rows == ""){
            print_message("ERROR", "No valid <rows> variables provided. Tabulation will be aborted.")
            return(invisible(NULL))
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Column variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Enable the use of macro variables
    columns <- apply_macro(columns)

    # Expand brackets first to be able to identify the unique variables used afterwards
    column_expand <- expand_brackets(columns)

    if (column_expand[[2]]){
        columns <- names(column_expand[[1]])
    }
    else{
        column_expand <- NULL
    }

    # Get row variables from provided combinations
    col_vars <- unlist_variables(columns)

    if (is.null(col_vars)){
        print_message("ERROR", "<Columns> variables must be provided in quotation marks. Tabulation will be aborted.")
        return(invisible(NULL))
    }

    col_vars <- data_frame |> part_of_df(col_vars, check_only = TRUE)

    if (is.list(col_vars)){
        print_message("ERROR", c("The provided <columns> variable[?s] '[vars]' [?is/are] not part of",
								 "the data frame. Tabulation will be aborted."), vars = col_vars[[1]])
        return(invisible(NULL))
    }

    # Make sure there is no column variable that is also a row variable.
    col_vars <- resolve_intersection(col_vars, row_vars, check_only = TRUE)

    if (is.list(col_vars)){
        print_message("ERROR", c("The provided <columns> variable[?s] '[vars]' [?is/are] also part of",
								 "the <rows> variables. Tabulation will be aborted."), vars = col_vars[[1]])
        return(invisible(NULL))
    }

    if (length(columns) <= 1){
        if (length(columns) == 0 || columns == ""){
            # Create empty pseudo variable to let the rest of the program run as normal
            data_frame[[".temp.var"]] <- 1
            columns    <- ".temp.var"
            col_vars   <- ".temp.var"
            var_labels <- c(var_labels, ".temp.var" = "")
            formats[[".temp.var"]] <- suppressMessages(discrete_format(" " = 1))
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # By variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Convert to character vectors
    by <- get_origin_as_char(by, substitute(by))

    # Make sure that the variables provided are part of the data frame.
    by <- data_frame |> part_of_df(by)

    # Make sure there is no row/column variable that is also a by variable.
    variables  <- c(row_vars, col_vars)
    by <- resolve_intersection(by, variables, check_only = TRUE)

    if (is.list(by)){
        print_message("WARNING", c("The provided <by> variable[?s] '[vars]' [?is/are] also part of",
								   "the <rows> or <columns> variables, which is not allowed. <by> will be removed."), vars = by[[1]])
        by <- c()
    }

    if (length(by) == 1){
        if (by == ""){
            print_message("WARNING", "No valid <by> variables provided. <by> will be removed.")
            by <- c()
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Weight
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    weight     <- get_origin_as_char(weight, substitute(weight))
    data_frame <- data_frame |> check_weight(weight)
    weight_var <- ".temp_weight"

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Statistics
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Translate statistics if possible
    temp_statistics <- tryCatch({
        # Force evaluation to see if it exists
        get_origin_as_char(statistics, substitute(statistics))
    }, error = function(e){
        # Evaluation failed
        NULL
    })

    # In case statistics couldn't be evaluated, this means a list was passed,
    # which provides specific variables for specific statistics. This list needs
    # to be evaluated differently.
    vars_per_stat_list <- NULL

    if (is.null(temp_statistics)){
        vars_per_stat_list <- lapply(as.list(substitute(statistics)), function(element){
            element <- tolower(as.character(element))

            element[!element %in% "c"]
        })[-1]

        # Set to NULL again on empty list
        if (length(vars_per_stat_list) == 0){
            vars_per_stat_list <- NULL
            statistics         <- NULL
        }
        # If a real list was passed
        else{
            statistics <- names(vars_per_stat_list)
        }
    }
    # Prepare statistics vector as normal
    else{
        statistics <- tolower(temp_statistics)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Values
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Convert to character vectors
    values <- get_origin_as_char(values, substitute(values))

    # If no value variables are provided generate them automatically
    if (length(values) <= 1){
        if (length(values) == 0 || values == ""){
            # If a statistics list with variable names is given, extract them an set
            # them as value variables.
            if (!is.null(vars_per_stat_list)){
                values        <- collapse::funique(unlist(vars_per_stat_list))
                names(values) <- NULL
            }
            # If no value variables are provided generate a temporary variable which
            # outputs unweighted results.
            else{
                values               <- ".temp_values"
                var_labels           <- c(var_labels, .temp_values = "")
                data_frame[[values]] <- 1
            }
        }
    }

    # Make sure there is no row/column variable that is also a value variable.
    values <- resolve_intersection(values, variables, check_only = TRUE)

    if (is.list(values)){
        print_message("ERROR", c("The provided <rows>/<columns> variable[?s] '[vars]' [?is/are] also part of",
								 "the <values> variables. Tabulation will be aborted."), vars = values[[1]])
        return(invisible(NULL))
    }

    # Make sure that the variables provided are part of the data frame.
    values <- data_frame |> part_of_df(values)

    if (length(values) == 0){
        print_message("ERROR", "No valid <values> variables provided. Tabulation will be aborted.")
        return(invisible(NULL))
    }

    values <- remove_doubled_values(values)

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Output
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Silent conversion of global options, which are invalid for any_table
    if (tolower(output) %in% c("console", "text")){
        output <- "excel"
    }

    # Check for invalid output option
    if (!tolower(output) %in% c("excel", "excel_nostyle")){
        print_message("WARNING", "<Output> format '[output]' not available. Using 'excel' instead.", output = output)

        output <- "excel"
    }
    else{
        output <- tolower(output)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Pre summarised data
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    pre_summed <- data_frame |> is_pre_summed(c(by, variables))

    # Extensiosn to preserve
    extensions <- c("_sum", "_pct_group_", "_pct_total", "_pct_value", "_pct_block", "_pct",
                    "_freq_g0", "_freq", "_mean", "_median", "_mode", "_min", "_max", "_first",
                    "_last", "_sum_wgt", "_p[0-9]+$", "sum_wgt", "_sd", "_variance", "_missing",
                    "by_vars", "TYPE_NR")

    # In case of using a pre summarised data frame, underscores are only allowed, if they carry
    # the statistics extension afterwards.
    if (pre_summed){
        # This basically serves as the default parameter
        if (length(statistics) == 0){
            statistics <- "sum"
        }

        # Only keep provided variables, otherwise it can happen later on that e.g. too many
        # variables are pivoted
        data_frame <- data_frame |> keep(by, variables, values, ".temp_weight")

        # If the TYPE variable isn't present in the data frame it will be generated here from
        # the provided variables.
        if (!"TYPE" %in% names(data_frame)){
            # For each observation check the non NA variables and combine their names in the TYPE
            # variable.
            data_frame[["TYPE"]] <- apply(data_frame[c(by, variables)], 1,
                function(variable){
                    var_name <- names(variable)[!is.na(variable)]

                    if (length(var_name) == 0){
                        NA_character_
                    }
                    else{
                        paste(var_name, collapse = "+")
                    }
                })

            data_frame |> data.table::setcolorder(c(by, variables, "TYPE"))
        }

        # Check if value variables have statistics extension
        pattern <- paste0("(", paste(extensions, collapse = "|"), ")$")

        # If one of the value variables hasn't got any of the above extension, add extensions automatically as provided
        if (!all(grepl(pattern, values))){
            print_message("WARNING", c("All <values> variables need to have the <statistic> extension in their variable name.",
									   "You can use the function 'add_extension' to achieve this. Provided <statistic> will be used.",
									   "This can lead to a wrong table structure, better add extensions by yourself."))

            data_frame <- data_frame |> add_extension(length(c(by, variables, "TYPE")) + 1,
                                                      rep(statistics, length(values))[1:length(values)])
        }

        # Set up options to make sure nothing errors below. Statistics is set to "mean"
        # because then summarise_plus takes a route where factor variables are kept in order.
        # With "sum" the order would be messed up.
        statistics <- "mean"
        pct_group  <- ""
        pct_value  <- ""
        weight     <- NULL
        formats    <- list()

        rm(pattern)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Order
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    order_by <- get_origin_as_char(order_by, substitute(order_by))

    # Correct ordering option if not set right
    order_by_vars <- NULL
    df_names      <- names(data_frame)

    # If only one argument is passed, it is assumed that it is one of the presets
    if (length(order_by) == 1){
        if (!tolower(order_by) %in% c("values", "stats", "values_stats", "columns", "interleaved", "blocks")){
            # If no preset is passed, check if a single variable name was passed
            order_by_vars    <- order_by[order_by %in% df_names]
            invalid_order_by <- order_by[!order_by %in% df_names]

            # No preset and no variable name was passed
            if (length(invalid_order_by) > 0){
                print_message("WARNING", c("<Order by> option '[order_by]' doesn't exist. Options 'values', 'stats', 'values_stats', 'columns'",
                                           "and 'interleaved' are available. <Order by> will be set to 'stats'."), order_by = order_by)
            }

            order_by <- "stats"
        }
    }
    # In case variable names were passed, format them with the underscore preserving
    # so that they match the pivoted variable names later on.
    else{
        order_by_vars <- replace_except(order_by, "_", "!!!", extensions)
        order_by      <- "stats"
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Statistics
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    pct_group <- get_origin_as_char(pct_group, substitute(pct_group))

    # If pct_group parameter is passed but is not selected as statistic, add it
    if (length(pct_group) > 0 && !"pct_group" %in% tolower(statistics)){
        if (!(length(pct_group) == 1 && pct_group == "")){
            statistics <- c(statistics, "pct_group")
        }
    }

    # If pct_group parameter is passed but is not selected as statistic, add it
    if (length(pct_value) > 0 && !"pct_value" %in% tolower(statistics)){
        if (!(length(pct_value) == 1 && pct_value == "")){
            statistics <- c(statistics, "pct_value")
        }
    }
    else if(length(pct_value) == 0 && "pct_value" %in% tolower(statistics)){
        print_message("NOTE", c("'pct_value' is specified in the statistics but the <pct_value> parameter",
                                " isn't set. 'pct_value' will be removed from statistics."))

        statistics <- statistics[!statistics %in% "pct_value"]
    }

    # If pct_block parameter is passed but is not selected as statistic, add it
    if (length(pct_block) > 0 && !"pct_block" %in% tolower(statistics)){
        if (!(length(pct_block) == 1 && pct_block == "")){
            statistics <- c(statistics, "pct_block")
        }
    }
    else if(length(pct_block) == 0 && "pct_block" %in% tolower(statistics)){
        print_message("NOTE", c("'pct_block' is specified in the statistics but the <pct_block> parameter",
                                " isn't set. 'pct_block' will be removed from statistics."))

        statistics <- statistics[!statistics %in% "pct_block"]
    }

    if (!pre_summed){
        list_of_statistics <- get_complete_statistics_list(statistics)

        # Check for invalid statistics
        valid_stats   <- statistics[statistics %in% c(names(list_of_statistics), "sum_wgt", "pct_value",
                                                      "pct_group", "pct_total", "pct_block", paste0("p", 1:100))]
        invalid_stats <- statistics[!statistics %in% c(names(list_of_statistics), "sum_wgt", "pct_value",
                                                       "pct_group", "pct_total", "pct_block", paste0("p", 1:100))]

        if (length(invalid_stats) > 0){
            print_message("WARNING", "<Statistic> '[invalid]' [?is/are] invalid and will be omitted.", invalid = invalid_stats)

            if (length(valid_stats) == 0){
                print_message("WARNING", "No valid <statistic> selected. 'sum' will be used.")

                statistics <- "sum"
            }
        }

        # This basically serves as the default parameter
        if (length(statistics) == 0){
            statistics <- "sum"
        }

        rm(list_of_statistics, invalid_stats)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Percentages
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    row_pct <- FALSE
    col_pct <- FALSE
    reenter_pct_group <- FALSE

    # Remove missing variables from pct_group
    if ("pct_group" %in% tolower(statistics)){
        # Keep groups for later sorting in case row or column percents where chosen
        order_pct <- sub("_pct", "", pct_group)

        invalid_pct <- pct_group[!pct_group %in% c(row_vars, col_vars)]

        # Check whether keywords for row or column percents where passed
        if (any(c("row_pct", "col_pct") %in% invalid_pct)){
            if ("row_pct" %in% invalid_pct){
                row_pct <- TRUE
            }
            if ("col_pct" %in% invalid_pct){
                col_pct <- TRUE
            }
        }

        if (length(invalid_pct) > 0){
            if (row_pct || col_pct){
                invalid_pct <- invalid_pct[!invalid_pct %in% c("row_pct", "col_pct")]
                reenter_pct_group <- TRUE
            }

            if (length(invalid_pct) > 0){
                print_message("WARNING", c("The variable[?s] '[invalid]' provided as <pct_group> [?is/are] not part",
										   "of the <rows> and <columns> variables. The variable[?s] will be omitted."), invalid = invalid_pct)

            }

            pct_group <- pct_group[pct_group %in% c(row_vars, col_vars)]

            # Save original statistics so that later on the order can be restored if
            # only row or column percentages were used.
            if (length(pct_group) == 0){
                statistics_orig <- statistics
                statistics      <- statistics[!statistics %in% "pct_group"]
            }
        }

        rm(invalid_pct)
    }

    # If pct_value is selected, make sure sum is also part of statistics
    flag_remove_sum <- FALSE

    if (!"sum" %in% statistics){
        if ("pct_value" %in% tolower(statistics) || row_pct || col_pct){
            statistics      <- c(statistics, "sum")
            flag_remove_sum <- TRUE
        }
    }

    # If pct_block is selected, make sure sum is also part of statistics
    flag_remove_block_sum <- FALSE

    pct_block <- get_origin_as_char(pct_block, substitute(pct_block))

    if (!all(pct_block %in% c("rows", "columns"))){
        print_message("WARNING", c("Invalid option for <pct_block>. <pct_block> can be 'rows' or 'columns', 'rows' will be used."))
        pct_block  <- "rows"
    }

    if (!"sum" %in% statistics){
        if ("pct_block" %in% tolower(statistics)){
            statistics            <- c(statistics, "sum")
            flag_remove_block_sum <- TRUE
        }
    }
    else{
        if ("pct_block" %in% tolower(statistics) && flag_remove_sum){
            flag_remove_block_sum <- TRUE
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Resolve macros
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    titles      <- apply_macro(titles)
    footnotes   <- apply_macro(footnotes)
    var_labels  <- apply_macro(var_labels)
    stat_labels <- apply_macro(stat_labels)

    ###########################################################################
    # Any tabulation starts
    ###########################################################################

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_next("Summary", "Summary")
    #-------------------------------------------------------------------------#
    print_step("MAJOR", "Computing stats")
    print_step("Minor", "Summarise")

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Some preparations beforehand
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Put together vector of grouping variables
    group_vars <- c(by, variables)

    # In case of group percentages order group variable to the last position
    if ("pct_group" %in% tolower(statistics) && length(pct_group) > 0){
        group_vars <- c(setdiff(group_vars, pct_group[1]), pct_group[1])
    }
    # If pct_group is specified in statistics but no group is provided set
    # last variable of group_vars as standard.
    else if ("pct_group" %in% tolower(statistics) && length(pct_group) == 0){
        pct_group <- group_vars[length(group_vars)]
    }

    # Put combinations in a single vector
    combinations <- as.vector(outer(rows, columns, paste, sep = " + "))

    # In case by variables are specified, add by to group variables and build
    # additional combinations
    if (length(by) > 0){
        combinations <- as.vector(outer(by, combinations, paste, sep = "+"))
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Summarise data according to provided variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Compute statistics
    if (!pre_summed){
        any_tab <- suppressMessages(data_frame |>
              summarise_plus(class      = group_vars,
                             values     = values,
                             statistics = statistics,
                             formats    = formats,
                             weight     = weight_var,
                             nesting    = "all",
                             types      = combinations,
                             notes      = FALSE,
                             na.rm      = na.rm,
                             print_miss = print_miss)) |>
            rename_pattern("pct_group", paste0("pct_group_", pct_group[1])) |>
            collapse::fsubset(TYPE != "total")
    }
    else{
        # With pre summarised data just take the input data frame
        any_tab <- data_frame
    }

    if (is.null(any_tab)){
        print_message("ERROR", "Table could not be computed. Execution will be aborted.")
        return(invisible(NULL))
    }

    # Underscores are not allowed in column variables because when constructing the
    # table header later, the underscore is the sign by which the column names are split.
    # Having additional underscores would mess up this part and lead to errors.
    if (any(grepl("_", unlist(any_tab[col_vars])))){
        print_message("ERROR", "No underscores allowed in <columns> variable values. Execution will be aborted.")
        return(invisible(NULL))
    }

    # Convert grouping variables to factor to retain value order after sorting later on
    any_tab <- any_tab |> convert_factor(group_vars)

    # In case of pre summarised data frame remove the temporary weight variable
    if (pre_summed){
        any_tab <- any_tab |> collapse::fselect(-.temp_weight)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Handle by variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # In case of by variables fuse them into one
    if (length(by) > 0){
        any_tab <- any_tab |> fuse_variables("by_vars", by)

        # Normally extract the first variable from the TYPE combination into a
        # separate BY variable.
        if (!pre_summed){
            any_tab[["BY"]]   <- sub("\\+.*", "", any_tab[["TYPE"]])
            any_tab[["TYPE"]] <- sub("^[^+]*\\+\\s*", "", any_tab[["TYPE"]])
        }
        # In case of pre summarised data frame it is not clear, at which position
        # the by variable is in the TYPE.
        else{
            # Get TYPE variable as list of variable combinations
            type_split <- strsplit(any_tab[["TYPE"]], "\\+")

            # If by variable was found in a variable combination, put it into
            # a separate variable.
            any_tab[["BY"]] <- vapply(type_split, function(obs){
                by_found <- obs[obs %in% by]

                if (length(by_found) > 0){
                    by_found[1]
                }
                else{
                    ""
                }
            }, character(1L))

            # The TYPE variable itself is put back together as all variables which
            # are no by-Variables.
            any_tab[["TYPE"]] <- vapply(type_split, function(obs){
                paste(obs[!obs %in% by], collapse = "+")
            }, character(1L))
        }

        # Convert to factor to keep them in order when sorting later
        any_tab <- any_tab |> convert_factor(c("BY", "by_vars"))
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Handle row percentages
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    if (any(grepl("pct", statistics))){ print_step("Minor", "Percentages") }

    # In case keyword row_pct was entered as group percentage, compute percentages
    # based on the row totals.
    if (row_pct){
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Row percentages", "Summary")
        #---------------------------------------------------------------------#

        current_values <- paste0(values, "_sum")

        # To make the column variable independent row percentages work, the data frame
        # has to be summarised as before, but the column variables don't receive their
        # formats. This is to prevent multilabels from doubling up the sums.
        # This summarise is important because it makes sure, that the row variables have
        # the same NA values as the main any_tab.
        row_formats <- formats[names(formats) %in% c(by, row_vars)]

        group_df <- suppressMessages(data_frame |>
              summarise_plus(class      = group_vars,
                             values     = values,
                             statistics = "sum",
                             formats    = row_formats,
                             weight     = weight_var,
                             nesting    = "all",
                             types      = combinations,
                             notes      = FALSE,
                             na.rm      = na.rm,
                             print_miss = print_miss)) |>
            rename_pattern("_sum", "")


        # The TYPE is put into the grouping, otherwise the row variable expressions
        # would double up.
        row_group <- c(row_vars, "TYPE")

        # Clean up by variables
        if (length(by) > 0){
            group_df <- group_df |> fuse_variables("by_vars", by)

            group_df[["TYPE"]] <- sub("^[^+]*\\+\\s*", "", group_df[["TYPE"]])

            row_group <- c("by_vars", row_vars, "TYPE")
        }

        # Generate a pseudo weight for a simple summarise
        group_df[[".weight"]] <- 1

        result_list <- group_df |>
            matrix_summarise(values,
                             row_group,
                             group_df[[".weight"]],
                             "sum",
                             get_complete_statistics_list("sum"),
                             monitor_df,
                             FALSE)

        group_df <- result_list[[1]] |>
            rename_pattern("_sum", "_row_sum")

        # Join total sums to the main data frame
        any_tab <- any_tab |>
            collapse::join(group_df,
                           on      = row_group,
                           how     = "left",
                           verbose = FALSE,
                           overid  = 2)

        # Calculate row percentages per value variable
        total_sums <- paste0(values, "_row_sum")

        for (i in seq_along(values)){
            new_variable <- paste0(values[i], "_pct_group_row")

            any_tab[[new_variable]] <-
                data.table::fifelse(any_tab[[total_sums[i]]] > 0,
                                    any_tab[[current_values[i]]] * 100 / any_tab[[total_sums[i]]],
                                    NA)
        }

        # Drop total sums
        any_tab <- any_tab |> dropp(":_row_sum")

        rm(row_group, result_list, group_df, current_values, total_sums, new_variable)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Handle column percentages
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # In case keyword col_pct was entered as group percentage, compute percentages
    # based on the row totals.
    if (col_pct){
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Column percentages", "Summary")
        #---------------------------------------------------------------------#

        current_values <- paste0(values, "_sum")

        # To make the row variable independent column percentages work, the data frame
        # has to be summarised as before, but the row variables don't receive their
        # formats. This is to prevent multilabels from doubling up the sums.
        # This summarise is important because it makes sure, that the row variables have
        # the same NA values as the main any_tab.
        col_formats <- formats[names(formats) %in% c(by, col_vars)]

        group_df <- suppressMessages(data_frame |>
             summarise_plus(class      = group_vars,
                            values     = values,
                            statistics = "sum",
                            formats    = col_formats,
                            weight     = weight_var,
                            nesting    = "all",
                            types      = combinations,
                            notes      = FALSE,
                            na.rm      = na.rm,
                            print_miss = print_miss)) |>
            rename_pattern("_sum", "")


        # The TYPE is put into the grouping, otherwise the row variable expressions
        # would double up.
        col_group <- c(col_vars, "TYPE")

        # Clean up by variables
        if (length(by) > 0){
            group_df <- group_df |> fuse_variables("by_vars", by)

            group_df[["TYPE"]] <- sub("^[^+]*\\+\\s*", "", group_df[["TYPE"]])

            col_group <- c("by_vars", col_vars, "TYPE")
        }

        # Generate a pseudo weight for a simple summarise
        group_df[[".weight"]] <- 1

        result_list <- group_df |>
            matrix_summarise(values,
                             col_group,
                             group_df[[".weight"]],
                             "sum",
                             get_complete_statistics_list("sum"),
                             monitor_df,
                             FALSE)

        group_df <- result_list[[1]] |>
            rename_pattern("_sum", "_col_sum")

        # Join total sums to the main data frame
        any_tab <- any_tab |>
            collapse::join(group_df,
                           on      = col_group,
                           how     = "left",
                           verbose = FALSE,
                           overid  = 2)

        # Calculate row percentages per value variable
        total_sums <- paste0(values, "_col_sum")

        for (i in seq_along(values)){
            new_variable <- paste0(values[i], "_pct_group_col")

            any_tab[[new_variable]] <-
                data.table::fifelse(any_tab[[total_sums[i]]] > 0,
                                    any_tab[[current_values[i]]] * 100 / any_tab[[total_sums[i]]],
                                    NA)
        }

        # Drop total sums
        any_tab <- any_tab |> dropp(":_col_sum")

        rm(col_group, result_list, group_df, current_values, total_sums, new_variable)
    }

    if (reenter_pct_group && !"pct_group" %in% statistics){
        statistics <- statistics_orig
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Handle group percentages
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # In case multiple group percentages should be computed, evaluate them in a loop
    # and join them to the main data frame.
    if (length(pct_group) > 1){
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Additional group pct", "Summary")
        #---------------------------------------------------------------------#

        # Pre summarise data frame in order to compute additional group percentages faster.
        # This basically is the same approach as in the shortcut route of summarise_plus.
        current_values <- paste0(values, "_sum")

        group_df <- data_frame |>
            matrix_summarise(values,
                             group_vars,
                             data_frame[[weight_var]],
                             "sum",
                             get_complete_statistics_list("sum"),
                             monitor_df,
                             FALSE)

        group_df <- group_df[[1]] |> collapse::frename(stats::setNames(current_values, values))

        group_df[[".temp_weight"]] <- 1

        # Compute additional group percentages one by one
        for (group in seq_along(pct_group)){
            # First group was computed before so omit it here
            if (group == 1){
                next
            }

            # In case of group percentages order group variable to the last position
            group_vars <- c(setdiff(group_vars, pct_group[group]), pct_group[group])

            # Compute group percentages
            group_tab <- suppressMessages(group_df |>
                summarise_plus(class      = group_vars,
                               values     = values,
                               statistics = "pct_group",
                               formats    = formats,
                               weight     = weight_var,
                               nesting    = "all",
                               types      = combinations,
                               notes      = FALSE,
                               na.rm      = na.rm,
                               print_miss = print_miss)) |>
                rename_pattern("pct_group", paste0("pct_group_", pct_group[group])) |>
                drop_type_vars()

            merge_vars <- variables

            if (length(by) > 0){
                group_tab  <- group_tab |> fuse_variables("by_vars", by)
                merge_vars <- c("by_vars", variables)
            }

            # Join percentages to the main data frame
            any_tab <- any_tab |>
                collapse::join(group_tab,
                               on      = merge_vars,
                               how     = "left",
                               verbose = FALSE,
                               overid  = 2)
        }
        rm(group, group_tab, group_df, current_values)
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Handle percentages based on value variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # In case percentages based on value variables should be computed
    if (length(pct_value) > 0){
        if (!(length(pct_value) == 1 && pct_value == "")){
            #---------------------------------------------------------------------#
            monitor_df <- monitor_df |> monitor_next("Value percentages", "Summary")
            #---------------------------------------------------------------------#

            for (i in seq_along(pct_value)){
                value <- pct_value[[i]]
                name  <- names(pct_value)[i]

                # Skip iteration because given operation is not a division of value
                # variables. Instead it will be a categorical percentage computation,
                # which will be handled in another step down below.
                if (grepl("/", value, fixed = TRUE)){
                    # Separate provided variables first
                    eval_vars <- trimws(strsplit(value, split = "/")[[1]])

                    # Compute percentages
                    if (paste0(eval_vars[1], "_sum") %in% names(any_tab) &&
                        paste0(eval_vars[2], "_sum") %in% names(any_tab)){
                        any_tab[[paste0(name, "_pct_value")]] <-
                            any_tab[[paste0(eval_vars[1], "_sum")]] * 100 /
                            any_tab[[paste0(eval_vars[2], "_sum")]]
                    }
                    # Without sum percentages can't be computed
                    else{
                        flag_remove_sum <- FALSE

                        # Additional warnings for missing variables
                        if (!eval_vars[1] %in% names(data_frame)){
                            print_message("WARNING", "Variable '[eval]' not found in the data frame.", eval = eval_vars[1])
                        }
                        if (!eval_vars[2] %in% names(data_frame)){
                            print_message("WARNING", "Variable '[eval]' not found in the data frame.", eval = eval_vars[2])
                        }
                    }
                }
                else{
                    if (!name %in% names(any_tab)){
                        print_message("WARNING", c("Variable '[name]' not found in the data frame.",
    											   "Percentages can't be computed."), name = name)
                        next
                    }

                    # Get the super group values by subsetting the data frame by the given
                    # variable value.
                    if (length(by) == 0){
                        super_group <- group_vars[!group_vars %in% name]
                    }
                    else{
                        super_group <- c("by_vars", group_vars[!group_vars %in% c(name, by)])
                    }

                    super_tab <- any_tab |>
                        collapse::fsubset(any_tab[[name]] == value) |>
                        keep(super_group, ":_sum") |>
                        rename_pattern("_sum", "_sum_super")

                    if (collapse::fnrow(super_tab) == 0){
                        print_message("WARNING", c("Subsetting variable '[name]' by '[value]' results in an empty data frame.",
    											   "Percentages can't be computed."), name = name, value = value)
                        next
                    }

                    # Join the super group values back to the full data frame
                    any_tab <- collapse::join(any_tab, super_tab,
                                              on       = super_group,
                                              how      = "left",
                                              multiple = TRUE,
                                              verbose  = FALSE,
    										  overid   = 2)

                    # Compute percentages
                    eval_vars <- grep("_sum$", names(any_tab), value = TRUE)

                    for (sum_var in eval_vars){
                        any_tab[[paste0(name, "_pct_value")]] <-
                            any_tab[[sum_var]] * 100 /
                            any_tab[[paste0(sum_var, "_super")]]
                    }

                    any_tab <- any_tab |> dropp(":_super")
                }
            }
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Handle percentages based on value variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # This capture enables the compute parameter to be written without quotation
    # marks.
    compute <- as.list(substitute(compute))[-1]

    if (length(compute) > 0){
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Individual calculations", "Summary")
        #---------------------------------------------------------------------#
        print_step("Minor", "Individual calculations")

        any_tab <- suppressMessages(any_tab |> compute.(compute))
    }

    # If no sums were selected in the statistics, drop them again now
    if (flag_remove_sum && !flag_remove_block_sum){
        any_tab <- any_tab |> dropp(":_sum")
    }

    # Abort if no values could be computed. This can especially happen during the various
    # percentage calculations if e.g. a wrong variable or expression was provided.
    if (length(by) == 0 && length(c(row_vars, col_vars, "TYPE", "TYPE_NR", "DEPTH")) == collapse::fncol(any_tab)){
        print_message("ERROR", c("After calculating the results, there are no valid values.",
								 "Tabulation will be aborted."))
        return(invisible(NULL))
    }

    if (length(by) > 0 && length(c(row_vars, col_vars, "by_vars", "TYPE", "TYPE_NR")) == collapse::fncol(any_tab)){
        print_message("ERROR", c("After calculating the results, there are no valid values.",
								 "Tabulation will be aborted."))
        return(invisible(NULL))
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Round values
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Reorder variables according to statistics. This is necessary because some
    # percentages can only be computed after summarise_plus and therefor aren't ordered.
    if ("pct_group" %in% statistics){
        any_tab <- any_tab |> setcolorder_by_pattern(order_pct)
    }

    any_tab <- any_tab |> setcolorder_by_pattern(statistics)

    # Get value variable names
    if (length(by) == 0){
        var_vector <- c(variables, "TYPE", "TYPE_NR", "DEPTH")
        value_vars <- any_tab |> inverse(var_vector)
    }
    else{
        var_vector <- c(variables, "TYPE", "TYPE_NR", "DEPTH", "by_vars", "BY")
        value_vars <- any_tab |> inverse(var_vector)
    }

    # Round values according to number formats
    for (var_name in value_vars){
        # Get stat from variable name
        stat <- strsplit(var_name, split = "_")[[1]]
        stat <- stat[length(stat)]

        # Round values to the decimals places specified in the style
        if (tolower(stat) %in% c("sum", "freq", "freq", "mean", "median", "mode",
                                 "min", "max")){
            any_tab[[var_name]] <- round_values(any_tab[[var_name]],
                                          style[["number_formats"]][[paste0(stat, "_decimals")]])
        }
        else if(stat == "g0"){
            any_tab[[var_name]] <- round_values(any_tab[[var_name]],
                                          style[["number_formats"]][["freq_decimals"]])
        }
        else if(stat == "wgt"){
            any_tab[[var_name]] <- round_values(any_tab[[var_name]],
                                          style[["number_formats"]][["sum_decimals"]])
        }
        else if(length(stat) <= 3 && substr(stat, 1, 1) == "p" && grepl("^[0-9]$", substr(stat, 2, 2))){
            any_tab[[var_name]] <- round_values(any_tab[[var_name]],
                                          style[["number_formats"]][["p_decimals"]])
        }
        else{
            any_tab[[var_name]] <- round_values(any_tab[[var_name]],
                                          style[["number_formats"]][["pct_decimals"]])
        }
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Tear apart the the summarised data frame by row and column combinations and
    # transpose columns to generate user defined combination. At the end put all
    # the pieces back together to form a fully printable result data frame.
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_next("Transform table", "Transform")
    #-------------------------------------------------------------------------#
    print_step("MAJOR", "Rearranging data for output table")

    part_combi_list       <- list()
    header_combi_list     <- list()
    col_header_df         <- list()
    row_header_dimensions <- list()
    last_number_of_rows   <- 0
    by_division           <- 1

    if (length(by) > 0){
        by_division <- length(collapse::funique(any_tab[["by_vars"]]))
    }

    # Underscores are not allowed in column and values variables, because when constructing the
    # table header later, the underscore is the symbol by which the variable names are split.
    # Having additional underscores would mess up this part and lead to errors. Therefor the
    # additional underscores will be temporarily replaced.
    rows       <- replace_except(rows,       "_", "!!!", extensions)
    columns    <- replace_except(columns,    "_", "!!!", extensions)
    value_vars <- replace_except(value_vars, "_", "!!!", extensions)
    value_sort <- replace_except(values,     "_", "!!!", extensions)
    names(any_tab)    <- replace_except(names(any_tab),    "_", "!!!", extensions)
    any_tab[["TYPE"]] <- replace_except(any_tab[["TYPE"]], "_", "!!!", extensions)

    # Sort type alphabetically to make finding the right combination easier
    any_tab[["TYPE"]] <- reorder_combination(any_tab[["TYPE"]])

    # Get number of column header variables by getting the maximum number of + signs in the
    # column variables.
    max_plus <- max(sapply(gregexpr("\\+", columns), function(var_to_test){
        if (var_to_test[1] == -1){
            1
        }
        else{
            length(var_to_test) + 1
        }}))

    index <- 1
    any_header <- NULL
    combined_id_vars <- NULL

    for (row_combi in rows){
        combined_col_df <- NULL

        # Get current single variables from row combination
        row_combi_vars <- collapse::funique(trimws(unlist(strsplit(row_combi, "\\+"))))

        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # For every combination of row variables loop through all column variable
        # combinations
        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        for (col_combi in columns){
            # Get current single variables from column combination
            col_combi_vars <- collapse::funique(trimws(unlist(strsplit(col_combi, "\\+"))))

            current_combi <- c(row_combi_vars, col_combi_vars)

            # Sort combination alphabetically
            sorted_combi <- paste(sort(current_combi), collapse = "+")

            # Keep only necessary variables
            if (length(by) > 0){
                current_combi <- c("BY", "by_vars", current_combi)
            }

            combi_df <- any_tab |> keep(current_combi, "TYPE", value_vars)

            # Combine variables to match TYPE variable and subset data frame by
            # current combination
            subset_type <- paste(sorted_combi, collapse = "+")

            # Sort to have the correct order after pivoting
            combi_df <- combi_df |>
                collapse::fsubset(TYPE == subset_type) |>
                data.table::setcolorder(current_combi, before = 1) |>
                data.table::setorderv(current_combi, na.last = TRUE)

            # When pre summarised data is used, it is possible to select types which
            # aren't present in the data frame. This leads to the data frame having no
            # observations at this point, which leads to an error down below. If this
            # happens, abort.
            if (collapse::fnrow(combi_df) == 0){
                print_message("ERROR", c("The variable combination of '[subset_type]' is not part of the provided data frame.",
										 "Tabulation will be aborted."), subset_type = subset_type)
                return(invisible(NULL))
            }

            # Rename the row variables to something neutral. In the next steps the
            # data frame is puzzled back together, but the thing is, that the row
            # variables can be individually in any order in the result table. If
            # this step would be omitted the variables would all be in a fixed order
            # in the result table.
            new_row_names <- paste0("var", seq_along(row_combi_vars))

            if (length(by) == 0){
                names(combi_df)[seq_along(row_combi_vars)] <- new_row_names

                id_vars <- new_row_names
            }
            else{
                end_col <- length(row_combi_vars) + 2
                names(combi_df)[3:end_col] <- new_row_names

                id_vars <- new_row_names
                id_vars <- c("BY", "by_vars", new_row_names)
            }

            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            # Calculate block percentages
            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

            # Make a copy of the value variables, because the vector is altered
            # during the block percentage calculation.
            pivot_values <- value_vars

            if (length(pct_block) > 0){
                #-------------------------------------------------------------------------#
                monitor_df <- monitor_df |> monitor_next("Block percentages", "Block percentages")
                #-------------------------------------------------------------------------#

                # Loop over rows and columns. The calculation is basically the same, but the
                # group variable selection is different.
                for (block in pct_block){
                    # On rows: select all  row variables but the last and all column variables
                    if (block == "rows"){
                        block_group  <- c(id_vars[-length(id_vars)], col_combi_vars)
                    }
                    # On columns: select all row variables and all column variables but the last
                    else{
                        block_group  <- c(id_vars, col_combi_vars[-length(col_combi_vars)])
                    }

                    # Get the sum variables and merge back the group maximums to the data frame
                    block_values <- pivot_values[endsWith(pivot_values, "_sum")]

                    combi_df <- suppressMessages(combi_df |>
                         summarise_plus(class      = block_group,
                                        values     = block_values,
                                        statistics = "max",
                                        notes      = FALSE,
                                        merge_back = TRUE))

                    # Run a do over loop to calculate all the block percentages for
                    # the different value variables.
                    new_pct  <- paste0(values, "_pct_block_", block)
                    max_vars <- paste0(block_values, "_max")

                    combi_df <- suppressMessages(combi_df |> if.(max_vars > 0, new_pct = block_values * 100 / max_vars))

                    # Add new percentages to values to pivot and remove the maximum variables
                    pivot_values <- c(pivot_values, new_pct)
                    combi_df     <- combi_df |> dropp(max_vars)
                }

                # Remove sum variables if they weren't specified in the statistics.
                # Also remove them from pivot vector.
                if (flag_remove_block_sum){
                    combi_df <- combi_df |> dropp(block_values)

                    pivot_values <- pivot_values[!pivot_values %in% block_values]
                }

                #-------------------------------------------------------------------------#
                monitor_df <- monitor_df |> monitor_next("Transform table", "Transform")
                #-------------------------------------------------------------------------#
            }

            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            # Main transposition
            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

            # If there is only one value provided and only one statistic selected
            # the pivoted variable names below only receive the column expressions
            # as names. The needed format is "value_stat_expression". In the mentioned
            # case this format is pre computed.
            if (length(values) == 1 && length(statistics) == 1){
                # If only group percentages are selected, but for multiple variables, then
                # the resulting variables are more than the given values, which isn't detected
                # with the condition above. This case has to be captured here, because it would
                # lead to wrong variable names and therefor a wrong header.
                if ((is.null(pct_group) || length(pct_group) <= 1) && !row_pct && !col_pct){
                    value_stat <- names(combi_df[collapse::fncol(combi_df)])

                    combi_df[[col_combi_vars[1]]] <-
                        paste0(value_stat, "_", combi_df[[col_combi_vars[1]]])
                }
            }
            # Fallback check if the user happens to input only wrong statistics but one.
            # In this case statistics is longer than 1 even though only one statistic was
            # computed. This leads to the condition above being omitted.
            else if (collapse::fncol(combi_df) - length(c(id_vars, col_combi_vars, "TYPE")) == 1){
                # If only group percentages are selected, but for multiple variables, then
                # the resulting variables are more than the given values, which isn't detected
                # with the condition above. This case has to be captured here, because it would
                # lead to wrong variable names and therefor a wrong header.
                if ((is.null(pct_group) || length(pct_group) <= 1) && !row_pct && !col_pct){
                    value_stat <- names(combi_df[collapse::fncol(combi_df)])

                    combi_df[[col_combi_vars[1]]] <-
                        paste0(value_stat, "_", combi_df[[col_combi_vars[1]]])
                }
            }

            # Pivot to wider format, which basically is the final format to print the data
            combi_df <- combi_df |>
                collapse::pivot(id     = id_vars,
                                names  = col_combi_vars,
                                values = pivot_values,
                                how    = "wider")

			# Remove excess underscore. This is created if no column variables are specified
            if (length(col_combi_vars) == 1 && col_combi_vars == ".temp.var"){
                names(combi_df) <- gsub("_ $", "", names(combi_df))
            }

            combi_df[id_vars] <- lapply(combi_df[id_vars], as.character)

            # Replace NA values with text so that they can be differentiated from empty
            # row header columns later on.
            row_var_cols <- seq_along(id_vars)
            combi_df[, row_var_cols][is.na(combi_df[, row_var_cols])] <- style[["na_symbol"]]

            # Sort interleaved
            if (tolower(order_by) == "interleaved"){
                combi_df <- combi_df |> order_interleaved(statistics)
            }

            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            # Join different column results together
            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

            if (is.null(combined_col_df)){
                row_labels       <- c()

                # Loop through all provided labels
                for (variable in row_combi_vars){
                    # Revert back underscores
                    variable <- gsub("!!!", "_", variable)

                    # If there are no labels specified add the variable names as labels
                    if (!variable %in% names(var_labels)){
                        row_labels <- c(row_labels, variable)
                        next
                    }

                    # Replace stat texts with provided labels
                    row_labels <- c(row_labels, var_labels[[variable]])
                }

				# Remove empty row labels so that no unnecessary slashes are created
                row_labels <- row_labels[row_labels != ""]

                # Insert labels as new variable and sort it to the front
                if (!all(row_labels == "")){
                    combi_df[["row.label"]] <- paste(row_labels, collapse = " / ")
                }
                # In case only empty labels were provided, just add empty character
                # so that the column can be identified as empty later on.
                else{
                    combi_df[["row.label"]] <- " "
                }

                # Order row label to the front
                if (length(by) == 0){
                    combi_df <- combi_df |>
                        data.table::setcolorder("row.label", before = 1)
                }
                else{
                    combi_df <- combi_df |>
                        data.table::setcolorder(c("BY", "by_vars", "row.label"), before = 1)
                }

                new_row_names <- c("row.label", new_row_names)

                # First iteration
                combined_col_df <- combi_df
            }
            # Following iterations
            else{
                # Check for duplicate variable names. If any duplicate is found abort.
                duplicates <- intersect(names(combined_col_df),
                                        setdiff(names(combi_df), c(new_row_names, "row.label", "BY", "by_vars")))

                if (length(duplicates) > 0){
                    print_message("ERROR", c("Duplicate <columns> names found: [dup].",
											 "If you are working with original values, consider making them unique by using formats."),
								  dup = paste(duplicates, collapse = ", "))
                    return(invisible(NULL))
                }

                # Join current data frame to the iterations before
                combined_col_df <- collapse::join(combined_col_df, combi_df,
                                                  on       = id_vars,
                                                  how      = "left",
                                                  verbose  = FALSE,
												  overid   = 2)

                # Remove variables which define the row labels so that they don't appear
                # when combining the column header below.
                combi_df <- suppressMessages(combi_df |>
                    dropp(new_row_names, "row.label", "BY", "by_vars"))
            }

            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            # Build variable name table header for later use during formatting.
            # Column header only needs to be built once because other iterations
            # would just produce the same header.
            #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

            if (index == 1){
                combined_id_vars <- id_vars

                # Get data frame variable names
                col_header_df <- combi_df[0, ]

                # Fill header rows with column variable names
                for (var_index in seq_along(col_combi_vars)){
                    col_header_df[var_index, ] <- gsub("!!!", "_", col_combi_vars[[var_index]])
                }

                # If the header has fewer rows than the maximum header rows, fill up the header
                # with additional empty rows.
                header_diff <- max_plus - collapse::fnrow(col_header_df)
                if (header_diff > 0){
                    col_header_df <- rbind(col_header_df,
                                           stats::setNames(data.table::as.data.table(
                                               matrix("", header_diff, collapse::fncol(col_header_df))),
                                               names(col_header_df)))
                }

                # First iteration
                if (is.null(any_header)){
                    # Remove row header columns from column header data frame
                    row_header_var_count <- length(id_vars) + 1
                    any_header <- col_header_df[, -(1:row_header_var_count), drop = FALSE]
                }
                # Following iterations
                else{
                    # cbind current header data frame to the iterations before
                    any_header <- cbind(any_header, col_header_df)
                }
            }
            else{
                # Gather row header id variables
                combined_id_vars <- union(combined_id_vars, id_vars)
            }
        }

        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # Combine data and store information
        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # Store combined column data frame in a list for later rbind
        part_combi_list[[row_combi]] <- combined_col_df

        # Get table dimensions for later use during formatting.
        name <- paste0("header_end_", paste(row_combi_vars, collapse = "%%%"))
        last_number_of_rows           <- (collapse::fnrow(combined_col_df) / by_division) + last_number_of_rows
        row_header_dimensions[[name]] <- last_number_of_rows

        name <- paste0("header_size_", paste(row_combi_vars, collapse = "%%%"))
        row_header_dimensions[[name]] <- length(row_combi_vars)

        index <- index + 1
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Put results together
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Put all computed data frames below each other to form a final result data frame.
    if (length(row_expand) == 0){
        any_tab <- data.table::rbindlist(part_combi_list, fill = TRUE, use.names = TRUE)
    }
    # If there are expanded brackets in the row combinations, also insert TYPE variable
    # back in to be able to sort the results.
    else{
        any_tab <- data.table::rbindlist(
            lapply(names(part_combi_list), function(combination){
                # Insert TYPE variable back in
                data_part           <- part_combi_list[[combination]]
                data_part[["TYPE"]] <- combination

                data_part
            }), fill = TRUE, use.names = TRUE)

        any_tab[["TYPE_ORIG"]] <- as.character(row_expand[any_tab[["TYPE"]]])
    }

    # If only specific variables should be kept per statistic, clean up the additional
    # not needed variables.
    if (!is.null(vars_per_stat_list)){
        variable_names <- names(any_header)

        # Generate search patterns like: variable name starts with list element
        # and somewhere in the variable name should appear the list element name.
        patterns <- sapply(names(vars_per_stat_list), function(name){
            elements <- vars_per_stat_list[[name]]
            paste0("^", elements, ".*", name, collapse = "|")
        })
        combined_pattern <- paste(patterns, collapse = "|")

        # Retrieve all variables to drop, which is the inverse vector of the patterns
        # that match. Drop these variables from the actual table and the header.
        vars_to_drop <- grep(combined_pattern, variable_names, value = TRUE, invert = TRUE)

        any_tab    <- suppressMessages(any_tab    |> dropp(vars_to_drop))
        any_header <- suppressMessages(any_header |> dropp(vars_to_drop))
    }

    # Reorder variables according to statistics
    if (tolower(order_by) == "stats" || tolower(order_by) == "values_stats" || tolower(order_by) == "blocks"){
        any_tab    <- any_tab    |> setcolorder_by_pattern(statistics)
        any_header <- any_header |> setcolorder_by_pattern(statistics)
    }

    # Reorder variables by provided values
    if (tolower(order_by) == "values" || tolower(order_by) == "values_stats"){
        any_tab    <- any_tab    |> setcolorder_by_pattern(value_sort)
        any_header <- any_header |> setcolorder_by_pattern(value_sort)
    }

    # Reorder variables by blocks depending on the root variable names
    if (tolower(order_by) == "blocks"){
        root_names <- collapse::funique(sub("_[^_]*$", "", names(any_header)))
        any_tab    <- any_tab    |> setcolorder_by_pattern(root_names)
        any_header <- any_header |> setcolorder_by_pattern(root_names)
    }

    if (length(order_by_vars) > 0){
        any_names <- names(any_tab)

        # Select all variables in blocks of the requested pattern
        ordered_cols <- unlist(lapply(order_by_vars, function(single_pattern){
            # Returns all variable names that match the provided pattern
            grep(single_pattern, any_names, value = TRUE, ignore.case = TRUE)
        }))

        # Put the ordered columns at the front of the data frame
        any_tab    <- any_tab    |> data.table::setcolorder(c(combined_id_vars, ordered_cols))
        any_header <- any_header |> data.table::setcolorder(ordered_cols)
    }
    else if (length(compute) > 0){
        any_names  <- replace_except(names(compute), "_", "!!!", extensions)

        any_tab    <- any_tab    |> setcolorder_by_pattern(any_names)
        any_header <- any_header |> setcolorder_by_pattern(any_names)
    }

    # After binding together the data frames it can happen, that some of the new var
    # variables end up at the end of the data frame instead of the front. Therefor
    # these columns need to be ordered to the front for safety.
    ordered_cols <- grep("^var[0-9]+$", names(any_tab), value = TRUE)
    any_tab <- any_tab |> data.table::setcolorder(c("row.label", ordered_cols), before = 1)

    # Sort row header columns if there are expanded combinations. What this does is
    # putting the combinations together in a way, so that what belongs together is
    # printed together. Meaning same combination roots are put below each other and
    # their cells get merged in the Excel table later.
    if (length(row_expand) > 0){
        # Sorting order: original not expanded group, all row header variables but the last
        #                expanded groups + last row header variable
        # This order keeps the root combinations together but the last variables
        # apart from another.
        sort_vars <- c("TYPE_ORIG", ordered_cols[-length(ordered_cols)],
                       "TYPE",      ordered_cols[length(ordered_cols)])

        # Get the unique values per variable in order of appearance. This is crucial
        # to keep the base order as is.
        levels_list <- lapply(any_tab[sort_vars], function(variable){
            collapse::funique(unlist(variable))
        })

        # Convert row header variables to factors
        for (variable in sort_vars){
            var_levels <- levels_list[[variable]]

            any_tab[[variable]] <- factor(any_tab[[variable]],
                                          levels  = var_levels,
                                          ordered = TRUE,
                                          exclude = NULL)
        }

        # Actual sorting
        data.table::setorderv(any_tab, sort_vars)

        # Convert back to character and drop TYPE variables
        any_tab[ordered_cols] <- lapply(any_tab[ordered_cols], as.character)
        any_tab <- any_tab |> collapse::fselect(-TYPE, -TYPE_ORIG)
    }

    # Get number of row header variables by getting the maximum number of + signs in the
    # row variables.
    max_plus <- max(sapply(gregexpr("\\+", rows), function(var_to_test){
        if (var_to_test[1] == -1){
            1
        }
        else{
            length(var_to_test) + 1
        }}))

    length_row_header <- max_plus + 1
    if (length(by) != 0){
        length_row_header <- length_row_header + 2
    }

    # Mark empty row header cells
    row_var_cols <- 1:length_row_header
    any_tab[, row_var_cols][is.na(any_tab[, row_var_cols])] <- ""

    # If all row labels are empty, delete the row header column
    if (all(any_tab[["row.label"]] == " ")){
        any_tab <- any_tab |> dropp("row.label")
    }
    # If only some labels are empty the row label column is printed. Problem: the cell
    # merging omits empty cells. Therefor convert empty cells into cells with a space
    # to merge the empty parts correctly.
    else{
        any_tab["row.label" == ""] <- " "
    }

    # Remove suppressed format expressions per do over for all row header variables
    # and drop column variables accordingly.
    any_tab <- suppressMessages(any_tab |>
                        if.(ordered_cols != "!:") |>
                        dropp(":_!:"))
    any_header <- suppressMessages(any_header |> dropp(":_!:"))

    # In between clean up to get a better overview
    rm(combi_df, combined_col_df, part_combi_list, col_combi, col_combi_vars,
       combinations, current_combi, index, last_number_of_rows, name, new_row_names,
       row_combi, row_combi_vars, sorted_combi, subset_type, group_vars, flag_remove_sum,
       length_row_header, col_header_df, header_diff, row_header_var_count, var_vector,
       var_name, var_index, value_sort, stat, row_pct, col_pct, pre_summed, ordered_cols,
       max_plus, id_vars, by_division, data_frame, pivot_values, extensions, combined_id_vars,
       order_by_vars)

    if (length(pct_block) > 0){
        rm(block_group, block_values, new_pct, max_vars)
    }

    # Grab all information, which is necessary to format the workbook. This list will be
    # returned at the end and can be grabbed by the workbook combine function.
    meta <- mget(c("rows", "columns", "statistics",
                   "by", "titles", "footnotes", "var_labels", "stat_labels",
                   "box", "any_header", "row_header_dimensions",
                   "style", "output", "na.rm", "print_miss"))

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Prepare table format for output
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_next("Excel prepare", "Format")
    #-------------------------------------------------------------------------#
    print_step("MAJOR", "Formatting table")

    # Setup styling in new workbook if no other is provided
    if (is.null(workbook)){
        workbook <- openxlsx2::wb_workbook() |>
            prepare_styles(list("title" = titles, "footnote" = footnotes), style)
    }
    # Update style options in provided workbook
    else{
        workbook <- workbook |>
            prepare_styles(list("title" = titles, "footnote" = footnotes), style)
    }

    monitor_df <- monitor_df |> monitor_end()

    # In case no by variables are provided
    if (length(by) == 0){
        wb_list <- format_any_excel(workbook, any_tab, rows, columns, statistics,
                                    by, titles, footnotes, var_labels, stat_labels,
                                    box, any_header,
                                    style, output, monitor_df = monitor_df)

        wb         <- wb_list[[1]]
        monitor_df <- wb_list[[2]]
    }
    # In case there are  by variables are provided
    else{
        if (!style[["by_as_subheaders"]]){
            wb_list <- format_any_by_excel(workbook, any_tab, rows, columns, statistics,
                                           by, titles, footnotes, var_labels, stat_labels,
                                           box, any_header,
                                           style, output, na.rm, print_miss, monitor_df)
        }
        else{
            wb_list <- format_by_single_table(workbook, any_tab, rows, columns, statistics,
                                              by, titles, footnotes, var_labels, stat_labels,
                                              box, any_header,
                                              style, output, na.rm, print_miss, monitor_df)
        }

        wb         <- wb_list[[1]]
        monitor_df <- wb_list[[2]]
    }

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Output formatted table into different formats
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    if (print){
        print_step("MAJOR", "Output table")

        monitor_df <- monitor_df |> monitor_next("Output tables", "Output tables")

        # Check if only save path or file name is specified. If only one is specified
        # print a note. Otherwise the file would not be saved and opened without a
        # hint to why the file wasn't saved.
        if (is.null(style[["save_path"]]) + is.null(style[["file"]]) == 1){
            if (is.null(style[["save_path"]])){
                print_message("NOTE", c("No save path specified. Both save path and file name with extension",
                                        "need to be specified in the global options or style parameter for",
                                        "the file to be saved. File won't be saved."))
            }
            else{
                print_message("NOTE", c("No file name specified. Both save path and file name with extension",
                                        "need to be specified in the global options or style parameter for",
                                        "the file to be saved. File won't be saved."))
            }
        }

        # If no save path or file provided just open workbook
        if (is.null(style[["save_path"]]) || is.null(style[["file"]])){
            if (interactive()){
                wb$open()
            }
        }
        else{
            # If save path doesn't exist, just open workbook
            if (!file.exists(style[["save_path"]])){
                print_message("WARNING", "Path does not exist: [style]", style = style[["save_path"]])

                if (interactive()){
                    wb$open()
                }
            }
            # Save file
            else{
                extension <- tolower(tools::file_ext(style[["file"]]))

                # CSV export is based on the actual data frame
                if (extension == "csv"){
                    any_tab |> data.table::fwrite(file = paste0(style[["save_path"]], "/", style[["file"]]),
                                                  sep  = ";",
                                                  dec  = ",")
                }
                # While XLSX export is based on the workbook
                else if (extension == "xlsx"){
                    wb$save(file = paste0(style[["save_path"]], "/", style[["file"]]), overwrite = TRUE)
                }
                # If there is no extension or a wrong one, auto correct it
                else{
                    wb$save(file = paste0(style[["save_path"]], "/", basename(style[["file"]]), ".xlsx"), overwrite = TRUE)
                }
            }
        }
    }

    print_closing(10)

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_end()
    monitor_df |> monitor_plot(draw_plot = monitor)
    #-------------------------------------------------------------------------#

    invisible(structure(list("table"    = any_tab,
                             "workbook" = wb,
                             "meta"     = meta), class = "qol_table"))
}


###############################################################################
# Format any table for excel output
###############################################################################
#' Format Any Table Output (Excel Based)
#'
#' @description
#' Format any table with the provided row and column variables. Statistics
#' can be anything available.
#'
#' @param wb An already created workbook to add more sheets to.
#' @param any_tab The data frame which contains the information for this table.
#' @param rows The variable that appears in the table rows.
#' @param columns The variable that appears in the table columns.
#' @param statistics The user requested statistics.
#' @param by Separate the cross table output by the expressions of the provided variables.
#' @param titles Character vector of titles to display above the table.
#' @param footnotes Character vector of footnotes to display under the table.
#' @param var_labels List which contains column variable names and their respective labels.
#' @param stat_labels List which contains statistic names and their respective labels.
#' @param box The text that should appear in the upper left box of the table.
#' @param any_header The column header carrying the variable names.
#' @param style A list containing the styling elements.
#' @param output Determines whether to style the output or to just quickly paste
#' the data.
#' @param by_info Text which contains the information which variable with which
#' expression is computed at the moment.Used for computation with by variables.
#' @param index Index of the current variable expression. Used for computation with
#' by variables.
#' @param monitor_df Data frame which stores the monitoring values.
#'
#' @return
#' Returns a list containing a formatted Excel workbook as well as the monitoring
#' data frame.
#'
#' @noRd
format_any_excel <- function(wb,
                             any_tab,
                             rows,
                             columns,
                             statistics,
                             by,
                             titles,
                             footnotes,
                             var_labels,
                             stat_labels,
                             box,
                             any_header,
                             style,
                             output,
                             by_info = NULL,
                             index   = NULL,
                             monitor_df){
    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_start("Excel prepare", "Format")
    #-------------------------------------------------------------------------#

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Setup header
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    if (is.null(by_info)){ print_step("MINOR", "Build header") }

    # Cut down percentage names to just "pct"
    names(any_tab) <- gsub("pct_group_", "pct group ",     names(any_tab))
    names(any_tab) <- gsub("pct_total",  "pct total",      names(any_tab))
    names(any_tab) <- gsub("pct_value",  "pct value",      names(any_tab))
    names(any_tab) <- gsub("pct_block_", "pct block ",     names(any_tab))
    names(any_tab) <- gsub("freq_g0",    "freq g0",        names(any_tab))
    names(any_tab) <- gsub("sum_wgt",    "weight_sum wgt", names(any_tab))

    # Build header from variable names
    multi_header <- build_multi_header(names(any_tab), any_header, var_labels, style)

    # Remove empty statistics rows, but keep multi_header with statistics because the information
    # is needed below for applying the correct number formats.
    column_header <- multi_header |> set_statistic_labels(stat_labels)
    column_header <- column_header[rowSums(column_header == "") != collapse::fncol(column_header), , drop = FALSE]

    stats_row <- multi_header[collapse::fnrow(multi_header), , drop = FALSE]

    # Get table ranges
    any_ranges <- get_any_tab_ranges(any_tab, column_header, stats_row,
                                     titles, footnotes, style)

    # Add empty columns to the header for the top left box at the beginning
    blank_columns <- matrix("", nrow = collapse::fnrow(column_header), ncol = any_ranges[["cat_col.width"]])
    column_header <- cbind(blank_columns, column_header)

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Display additional table information
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Add box text
    if (box != ""){
        column_header[1, 1] <- box
    }
    # If no box text provided put in variable names of row headers
    else{
        column_header[1, 1] <- gsub("!!!", "_", paste(rows, collapse = "\n"))
    }

    # If function is called with by variables the sheet names have to be differentiated
    # and by info has to be written above the table.
    if (!is.null(by_info)){
        if (style[["sheet_name"]] == "by"){
            wb$add_worksheet(by,
                             grid_lines = style[["grid_lines"]])
        }
        else{
            wb$add_worksheet(paste0(style[["sheet_name"]], index),
                             grid_lines = style[["grid_lines"]])
        }
    }
    else{
        wb$add_worksheet(style[["sheet_name"]], grid_lines = style[["grid_lines"]])
    }

    # Rename the following stats back to match number formats in style element
    names(any_tab) <- gsub("sum.wgt", "sum_wgt", names(any_tab))

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Add table data and format according to style options
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    if (is.null(by_info)){ print_step("MINOR", "Insert data") }

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_next("Excel data", "Format")
    #-------------------------------------------------------------------------#

    wb$add_data(x          = any_tab,
                start_col  = style[["start_column"]],
                start_row  = any_ranges[["table.row"]],
                col_names  = FALSE,
                na.strings = style[["na_symbol"]])


    # Add column header above table.
    wb$add_data(x          = column_header,
                start_col  = style[["start_column"]],
                start_row  = any_ranges[["header.row"]],
                col_names  = FALSE)

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Table styling
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    if (is.null(by_info)){ print_step("MINOR", "Format titles and footnotes") }

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_next("Excel titles/footnotes", "Format")
    #-------------------------------------------------------------------------#
    # Format titles and footnotes if there are any
    wb <- wb |>
        format_titles_foot_excel(titles, footnotes, any_ranges, style, output)

    # Only do the formatting when user specified it. With the excel_nostyle
    # option this whole part gets omitted to get a very quick unformatted
    # excel output.
    if (output == "excel"){
        if (is.null(by_info)){ print_step("MINOR", "Merge headers") }

        # Merge top left box
        wb$merge_cells(dims = any_ranges[["box_range"]])

        # Merge column and row headers
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Excel format col headers", "Format")
        #---------------------------------------------------------------------#
        wb <- wb |>
            handle_col_header_merge(column_header[, -c(1:any_ranges[["cat_col.width"]]), drop = FALSE], any_ranges, style)

        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Excel format row headers", "Format")
        #---------------------------------------------------------------------#
        wb <- wb |>
            handle_row_header_merge(any_tab[, 1:any_ranges[["cat_col.width"]]], any_ranges)

        # Fill all cells with background color
        if (style[["background_color"]] != ""){
            #-----------------------------------------------------------------#
            monitor_df <- monitor_df |> monitor_next("Excel background", "Format")
            #-----------------------------------------------------------------#
            if (is.null(by_info)){ print_step("MINOR", "Fill background") }

            wb$add_fill(color = openxlsx2::wb_color(style[["background_color"]]))
            wb$set_cell_style_across(cols = "A:XFD", style = wb$get_cell_style(dims = "A1"))

            # Titles and footnotes need to get an extra fill
            wb$add_fill(dims = any_ranges[["title_range"]],    color = openxlsx2::wb_color(style[["background_color"]]))
            wb$add_fill(dims = any_ranges[["footnote_range"]], color = openxlsx2::wb_color(style[["background_color"]]))
        }

        # Style table
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Excel cell styles", "Format")
        #---------------------------------------------------------------------#
        if (is.null(by_info)){ print_step("MINOR", "Add cell styles") }

        wb <- wb |> handle_cell_styles(any_ranges, style)

        # Format subheaders, if present
        if (style[["by_as_subheaders"]]){
            if (is.null(by_info)){ print_step("MINOR", "Add subheaders") }

            # Get subheader ranges and the ones of the empty column header
            subheader_range     <- any_ranges[["subheader_range"]]
            pre_subheader_range <- any_ranges[["pre_subheader_range"]]

            # Merge subheaders and apply style
            for (sub_range in seq_along(subheader_range)){
                wb$add_data(x          = style[["subheaders"]][[sub_range]],
                            start_col  = any_ranges[["header.column"]] + any_ranges[["cat_col.width"]],
                            start_row  = any_ranges[["table.row"]] + (style[["subheader_rows"]][[sub_range]] - 1),
                            col_names  = FALSE,
                            na.strings = style[["na_symbol"]])
            }

            wb$merge_cells(dims = pre_subheader_range)
            wb$merge_cells(dims = subheader_range)
        }

        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Excel number formats", "Format")
        #---------------------------------------------------------------------#
        if (is.null(by_info)){ print_step("MINOR", "Format numbers") }

        # Set up inner table number formats
        col_index <- 1
        chunks    <- sub("p[0-9]+$", "p", rle(stats_row)$values)

        for (type in chunks){
            wb$add_cell_style(dims                = any_ranges[[paste0("any_col_ranges", col_index)]],
                              apply_number_format = TRUE,
                              num_fmt_id          = wb$styles_mgr$get_numfmt_id(paste0(type, "_numfmt")))

            col_index <- col_index + 1
        }

        # Draw inner table cells as heat map with conditional formatting
        if (style[["as_heatmap"]]){
            #-----------------------------------------------------------------#
            monitor_df <- monitor_df |> monitor_next("Excel format heatmap", "Format")
            #-----------------------------------------------------------------#
            if (is.null(by_info)){ print_step("MINOR", "Add heatmap") }

            wb$add_conditional_formatting(dims  = any_ranges[["table_range"]],
                                          style = c(style[["heatmap_low_color"]],
                                                    style[["heatmap_middle_color"]],
                                                    style[["heatmap_high_color"]]),
                                          type  = "colorScale")
        }

        # Freeze headers. If both options are true they have to be set together, otherwise one
        # option would overwrite the other.
        if (style[["freeze_col_header"]] && style[["freeze_row_header"]]){
            wb$freeze_pane(first_active_col = any_ranges[["header.column"]] + any_ranges[["cat_col.width"]],
                           first_active_row = any_ranges[["table.row"]])
        }
        else if (style[["freeze_col_header"]]){
            wb$freeze_pane(first_active_col = any_ranges[["header.column"]] + any_ranges[["cat_col.width"]])
        }
        else if (style[["freeze_row_header"]]){
            wb$freeze_pane(first_active_row = any_ranges[["table.row"]])
        }

        # Adjust table dimensions
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Excel widths/heights", "Format")
        #---------------------------------------------------------------------#
        if (is.null(by_info)){ print_step("MINOR", "Adjust row heights and column widths") }

        wb <- wb |> handle_col_row_dimensions(any_ranges,
                                              collapse::fncol(any_tab),
                                              collapse::fnrow(any_tab) + collapse::fnrow(multi_header),
                                              style) |>
            handle_any_auto_dimensions(any_ranges, style) |>
            handle_header_table_dim(any_ranges, style)
    }

    wb$add_ignore_error(dims = any_ranges[["header_range"]],  number_stored_as_text = TRUE)
    wb$add_ignore_error(dims = any_ranges[["cat_col_range"]], number_stored_as_text = TRUE)
    wb$add_ignore_error(dims = any_ranges[["table_range"]],   number_stored_as_text = TRUE)

    wb$add_named_region(dims = any_ranges[["whole_tab_range"]], name = "table", local_sheet = TRUE)
    wb$add_named_region(dims = any_ranges[["table_range"]],     name = "data",  local_sheet = TRUE)

    monitor_df <- monitor_df |> monitor_end()

    # Return workbook
    list(wb, monitor_df)
}


#' Insert Statistic Labels
#'
#' @description
#' Give the statistics in the column header a custom label.
#'
#' @param column_header The complete column multi header.
#' @param stat_labels A list in which is specified which label should be printed for
#' which statistic instead of the statistic name.
#'
#' @return
#' Returns a multi layered column header with replaced statistic texts.
#'
#' @noRd
set_statistic_labels <- function(column_header, stat_labels){
    if (length(stat_labels) == 0){
        return(column_header)
    }

    # Loop through all provided labels
    for (i in seq_along(stat_labels)){
        name  <- names(stat_labels)[i]
        label <- stat_labels[[i]]

        # Omit label with missing variable name
        if (is.null(name) || name == ""){
            next
        }

        # Replace stat texts with provided labels
        column_header[collapse::fnrow(column_header), ] <- gsub(name, label, column_header[collapse::fnrow(column_header), ])
    }

    column_header
}


#' Insert Variable Labels
#'
#' @description
#' Give the variables in the column header a custom label.
#'
#' @param column_header The complete column multi header.
#' @param var_labels A list in which is specified which label should be printed for
#' which variable instead of the variable name.
#'
#' @return
#' Returns a multi layered column header with replaced variable texts.
#'
#' @noRd
set_col_variable_labels <- function(column_header, var_labels){
    if (length(var_labels) == 0){
        return(column_header)
    }

    # Order list by the length of the variable names (descending) to prevent that, if
    # the shortest variable name is at the top, and other variables follow with the
    # same base name (like: age, age1, age2), it would overwrite all other labels for
    # the following variables with the same base name.
    var_labels <- var_labels[order(nchar(names(var_labels)), decreasing = TRUE)]

    # Loop through all provided labels
    for (i in seq_along(var_labels)){
        name  <- names(var_labels)[i]
        label <- var_labels[[i]]

        # Omit label with missing variable name
        if (is.null(name) || name == ""){
            next
        }

        # Replace variable texts with provided labels
        column_header[,] <- gsub(paste0("^", name, "$"), label, as.matrix(column_header))
    }

    # If header only consists of one row it gets converted to a vector when using gsub above.
    # In case this happens, convert the vector back into a one row data frame.
    if (!is.matrix(column_header)){
        column_header <- data.table::as.data.table(as.list(column_header), stringsAsFactors = FALSE)
    }

    column_header
}


#' Build a Multi Header from Variable Names
#'
#' @description
#' Build a multi layered header from variable names of any_tab data frame. Analysis
#' variable name, stat and variable expressions are layerd into rows.
#'
#' @param var_names Variable names from any_tab.
#' @param any_header The column header carrying the variable names.
#' @param var_labels A list in which is specified which label should be printed for
#' which variable instead of the variable name.
#' @param style A list containing the styling elements.
#'
#' @return
#' Returns a data table with a multi layered header.
#'
#' @noRd
build_multi_header <- function(var_names,
                               any_header,
                               var_labels,
                               style){
    # Make sure no statistic is part of the variable labels
    extensions <- c("sum", "pctgroup", "pcttotal", "pctvalue", "pctblock", "pct",
                    "freqg0", "freq", "mean", "median", "mode", "min", "max", "first",
                    "last", "sumwgt", "p[0-9]+$", "sd", "variance", "missing")

    labels_to_drop <- grepl(paste(extensions, collapse = "|"), names(var_labels))

    var_labels <- var_labels[!labels_to_drop]

    # Replace variable texts with custom labels
    any_header      <- any_header |> set_col_variable_labels(var_labels)
    col_var_headers <- data.table::as.data.table(any_header, stringsAsFactors = FALSE)

    # Split up variable name into different parts
    header_parts <- strsplit(var_names, "_")
    max_parts    <- max(lengths(header_parts))

    # Remove row header variable names
    for (part in seq_along(header_parts)){
        if (length(header_parts[[part]]) > 1){
            break
        }

        header_parts[[part]] <- ""
    }

    # Loop through header parts
    header_matrix <- sapply(header_parts, function(parts){
        # Prepare empty header with max size
        multi_header <- rep("", max_parts)

        if (parts[1] != ""){
            # Identify different parts
            var_name    <- parts[1]
            stat        <- strsplit(parts[2], " ")[[1]][1]
            expressions <- parts[-c(1, 2)]

            # In case no column variables are provided, the expression will end
            # up as empty character vector. This is captured as empty character
            # of length 1, so that the header can be set up as usual.
            if (length(expressions) == 0){
                expressions <- ""
            }

            # Put multi header together: variable name, expressions, stat
            multi_header[1] <- var_name
            multi_header[seq(2, length(expressions) + 1)] <- expressions
            multi_header[max_parts] <- stat
        }

        multi_header
    })

    # Revert back underscores
    header_matrix <- gsub("!!!", "_", header_matrix)

    # Replace variable texts with custom labels
    header_matrix <- header_matrix |> set_col_variable_labels(var_labels)

    # Make sure header_matrix is treated as a matrix, even though there can be only one row
    if (is.null(dim(header_matrix))){
        dim(header_matrix) <- c(1, length(header_matrix))
    }

    # Drop completely empty rows
    if (collapse::fnrow(header_matrix) > 1){
        header_matrix <- data.table::as.data.table(
            header_matrix[, colSums(header_matrix != "" & header_matrix != " ") > 0, drop = FALSE])
    }
    else{
        header_matrix <- data.table::as.data.table(
            header_matrix[, colSums(header_matrix != "" & header_matrix != " "), drop = FALSE])
    }

    # Replace NA values
    header_matrix[is.na(header_matrix) | header_matrix == "NA"] <- style[["na_symbol"]]

    # Inject column variable headers into the multi header
    merged_header <- merge_headers(header_matrix, col_var_headers)

    # Get the more detailed blocks
    if (length(var_labels) > 0 && any(startsWith(tolower(names(var_labels)), "block"))){
        start_var <- (length(var_names) - collapse::fncol(merged_header)) + 1

        # Get the root variable names to count the number of group elements
        root_names    <- sub("^([^_]+_[^_]+)_.*$", "\\1", var_names[start_var:length(var_names)])
        block_lengths <- rle(root_names)[["lengths"]]

        # Generate block headlines
        block_labels <- var_labels[startsWith(tolower(names(var_labels)), "block")]
        block_labels <- fill_or_trim(block_labels, length(block_lengths))
        block_labels <- rep(block_labels, times = block_lengths)
        block_labels <- data.table::as.data.table(as.list(block_labels), stringsAsFactors = FALSE)

        # overwrite names of first table
        block_labels <- stats::setNames(block_labels, names(merged_header))

        # now rbind
        merged_header <- data.table::rbindlist(list(block_labels, merged_header), use.names = TRUE)
    }

    merged_header
}


#' Inject Variable Header into Multi Header
#'
#' @description
#' The multi layered headers based on variable values and variable names are generated
#' separately and need to be put together afterwards.
#'
#' @param value_header Header based on variable values.
#' @param variable_header Header based on variable names which will be injected.
#'
#' @return
#' Returns a data table with a complete multi layered header.
#'
#' @noRd
merge_headers <- function(value_header, variable_header){
    row_list <- list()

    # Loop over the value header rows to inject the variable headers into them
    for (i in seq_len(collapse::fnrow(value_header))){
        # Take a row from the value header
        row_list[[length(row_list) + 1]] <- value_header[i, , drop = FALSE]

        # If there are still rows in the variable header check whether to inject them
        if (i <= collapse::fnrow(variable_header)){
            variable_row <- variable_header[i, , drop = FALSE]

            # Only inject variable row, if the row isn't completely empty
            if (!all(variable_row == "")){
                row_list[[length(row_list) + 1]] <- variable_row
            }
        }
    }

    # Output as data table
    data.table::rbindlist(row_list, use.names = FALSE)
}


###############################################################################
# Format grouped by tables for excel output
###############################################################################
#' Format Any Table Output with by Variables (Excel Based)
#'
#' @description
#' Format any table with the provided row and column variables. Data frame is split
#' by by-variables, each table gets printed on a different sheet.
#'
#' @param wb An already created workbook to add more sheets to.
#' @param any_tab The data frame which contains the information for this table.
#' @param rows The variable that appears in the table rows.
#' @param columns The variable that appears in the table columns.
#' @param statistics The user requested statistics.
#' @param by Separate the cross table output by the expressions of the provided variables.
#' @param titles Character vector of titles to display above the table.
#' @param footnotes Character vector of footnotes to display under the table.
#' @param var_labels List which contains column variable names and their respective labels.
#' @param stat_labels List which contains statistic names and their respective labels.
#' @param box The text that should appear in the upper left box of the table.
#' @param any_header The column header carrying the variable names.
#' @param style A list containing the styling elements.
#' @param output Determines whether to style the output or to just quickly paste
#' the data.
#' @param na.rm If TRUE removes all NA values from the tabulation.
#' @param print_miss FALSE by default. If TRUE outputs all possible categories of the
#' grouping variables based on the provided formats, even if there are no observations
#' for a combination.
#' @param monitor_df Data frame which stores the monitoring values.
#'
#' @return
#' Returns a list containing a formatted Excel workbook as well as the monitoring
#' data frame.
#'
#' @noRd
format_any_by_excel <- function(wb,
                                any_tab,
                                rows,
                                columns,
                                statistics,
                                by,
                                titles,
                                footnotes,
                                var_labels,
                                stat_labels,
                                box,
                                any_header,
                                style,
                                output,
                                na.rm,
                                print_miss,
                                monitor_df){

    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # Loop through all by variables
    #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    index <- 1

    for (by_var in by){
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_start(paste0("Excel prepare (", by_var, ")"), "Format by")
        #---------------------------------------------------------------------#

        # Select by variables one by one
        any_by <- any_tab |>
            collapse::fsubset(any_tab[["BY"]] == by_var)

        # Extract unique values
        if (anyNA(any_by[["by_vars"]])){
            values <- c(collapse::funique(collapse::na_omit(any_by[["by_vars"]])), NA)
        }
        else{
            values <- collapse::funique(any_by[["by_vars"]])
        }

        monitor_df <- monitor_df |> monitor_end()

        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # Loop through all unique values to generate tables per expression
        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        for (value in values){
            # In case NAs are removed
            if (is.na(value) && na.rm){
                next
            }

            #-----------------------------------------------------------------#
            monitor_df <- monitor_df |> monitor_start(paste0("Excel (", by_var, "_", value, ")"), "Format by")
            #-----------------------------------------------------------------#
            print_step("MINOR", "[by] = [value]", by = by_var, value = value)

            # Put additional by info together with the information which by variable
            # and which value is currently filtered.
            by_info <- paste0(by_var, " = ", value)

            # Filter table by current by variable and value
            if (!is.na(value)){
                any_temp <- any_by |>
                    collapse::fsubset(any_by[["by_vars"]] == value)
            }
            else{
                any_temp <- any_by |>
                    collapse::fsubset(is.na(any_by[["by_vars"]]))
            }

            any_temp <- any_temp |> dropp("BY", "by_vars")

            # Add by info below the titles
            if (length(titles) > 0){
                titles_temp <- c(titles, "", by_info)
            }
            # Or on top if there are no titles
            else{
                titles_temp <- by_info
            }

            # The print_miss option enables a shortcut in formatting the sheets after
            # the first one. Since it guarantees that all follow up sheets are printed
            # with the exact same table width and height, because all categories are
            # printed, only the first sheet must be formatted. All other sheets can
            # clone the entire style from the first sheet.
            if (index == 1 || !print_miss){
                # Generate tables as normal but base is filtered data frame
                wb_list <- format_any_excel(wb,
                                            any_temp,
                                            rows,
                                            columns,
                                            statistics,
                                            value,
                                            titles_temp,
                                            footnotes,
                                            var_labels,
                                            stat_labels,
                                            box,
                                            any_header,
                                            style,
                                            output,
                                            by_info,
                                            index,
                                            NULL)

            }
            # Clone style for follow up iterations
            else{
                # Generate tables with no style but base is filtered data frame
                wb_list <- format_any_excel(wb,
                                            any_temp,
                                            rows,
                                            columns,
                                            statistics,
                                            value,
                                            titles_temp,
                                            footnotes,
                                            var_labels,
                                            stat_labels,
                                            box,
                                            any_header,
                                            style,
                                            "excel_nostyle",
                                            by_info,
                                            index,
                                            NULL)

                # Clone entire style from first sheet
                wb_list[[1]]$clone_sheet_style(from = 1, to = index)
            }

            index <- index + 1

            wb <- wb_list[[1]]

            monitor_df <- monitor_df |> monitor_end()
        }

        monitor_df <- monitor_df |> monitor_end()
    }

    # Return workbook
    list(wb, monitor_df)
}


###############################################################################
# Format grouped by tables for excel output in one table
###############################################################################
#' Format Any Table Output with by Variables On One Sheet (Excel Based)
#'
#' @description
#' Format any table with the provided row and column variables. Data frame is printed
#' as one on one sheet. By-variables are used as subheaders.
#'
#' @param wb An already created workbook to add more sheets to.
#' @param any_tab The data frame which contains the information for this cross
#' table.
#' @param rows The variable that appears in the table rows.
#' @param columns The variable that appears in the table columns.
#' @param statistics The user requested statistics.
#' @param by Separate the cross table output by the expressions of the provided variables.
#' @param titles Character vector of titles to display above the table.
#' @param footnotes Character vector of footnotes to display under the table.
#' @param var_labels List which contains column variable names and their respective labels.
#' @param stat_labels List which contains statistic names and their respective labels.
#' @param box The text that should appear in the upper left box of the table.
#' @param any_header The column header carrying the variable names.
#' @param style A list containing the styling elements.
#' @param output Determines whether to style the output or to just quickly paste
#' the data.
#' @param na.rm If TRUE removes all NA values from the tabulation.
#' @param print_miss FALSE by default. If TRUE outputs all possible categories of the
#' grouping variables based on the provided formats, even if there are no observations
#' for a combination.
#' @param monitor_df Data frame which stores the monitoring values.
#'
#' @return
#' Returns a list containing a formatted Excel workbook as well as the monitoring
#' data frame.
#'
#' @noRd
format_by_single_table <- function(wb,
                                   any_tab,
                                   rows,
                                   columns,
                                   statistics,
                                   by,
                                   titles,
                                   footnotes,
                                   var_labels,
                                   stat_labels,
                                   box,
                                   any_header,
                                   style,
                                   output,
                                   na.rm,
                                   print_miss,
                                   monitor_df){
    #---------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_start("Insert subheaders", "Format by")
    #---------------------------------------------------------------------#

    # Store the variable names for later
    var_names <- names(any_tab)

    # Get the unique by variable expressions as subheaders
    subheaders <- any_tab|>
        collapse::fselect("by_vars")|>
        collapse::funique("by_vars")

    style[["subheaders"]] <- subheaders[[1]]

    # Get number of row header variables by determining the number of character columns
    variable_types        <- vapply(any_tab, is.character, logical(1))
    number_of_row_headers <- which(!variable_types)[1] - 1
    number_of_values      <- collapse::fncol(any_tab) - number_of_row_headers

    # Split up data frame on by variable expressions
    grouping   <- collapse::GRP(any_tab, "by_vars", sort = FALSE)
    split_list <- any_tab |> collapse::gsplit(g = grouping, use.g.names = TRUE)

    # Insert by variable expressions as subheadings
    for (i in seq_along(split_list)){
        # Get subheader name and bind together as data frame row
        subheading <- split_list[[i]][1, "by_vars"]

        # Generate empty subheader line in separate parts to ensure that the value
        # columns will be set as numeric.
        sub_part1 <- as.list(rep(" ", number_of_row_headers))
        sub_part2 <- as.list(rep(NA_real_, number_of_values))

        # Combine both parts into data frame
        subheading_df <- data.table::as.data.table(c(sub_part1, sub_part2))

        # Put subheader above the initial data frame
        split_list[[i]] <- data.table::rbindlist(list(subheading_df, split_list[[i]]), use.names = FALSE)

        # Insert a helper column which indicates where in the data frame the headline sits.
        # This is later used to identify the subheader rows in the full data frame
        split_list[[i]][[".subheader"]]  <- 0
        split_list[[i]][1, ".subheader"] <- 1
    }

    # Bind full data frame back together
    any_tab <- data.table::rbindlist(split_list, use.names = FALSE)

    names(any_tab) <- c(var_names, ".subheader")
    any_tab <- any_tab |> collapse::fselect(-BY, -by_vars)

    # Set row numbers of subheaders in the helper column
    any_tab[[".subheader"]][any_tab[[".subheader"]] == 1] <- which(any_tab[[".subheader"]] == 1)

    subheader_rows <- any_tab |>
        collapse::fselect(".subheader") |>
        collapse::funique(".subheader") |>
        collapse::fsubset(.subheader != 0)

    style[["subheader_rows"]] <- subheader_rows[[1]]

    # Generate tables as normal but base is filtered data frame
    any_tab <- any_tab |> dropp(".subheader")

    monitor_df <- monitor_df |> monitor_end()

    wb_list <- format_any_excel(wb,
                                any_tab,
                                rows,
                                columns,
                                statistics,
                                NULL,
                                titles,
                                footnotes,
                                var_labels,
                                stat_labels,
                                box,
                                any_header,
                                style,
                                output,
                                monitor_df = monitor_df)

    # Return workbook
    list(wb_list[[1]], wb_list[[2]])
}


###############################################################################
# Combine multiple tables into one workbook
###############################################################################
#' Combine Multiple Tables Into One Workbook
#'
#' @description
#' Combines any number of tables created with [any_table()] into one workbook
#' and styles them according to their meta information.
#'
#' @param ... Provide any number of result lists output by [any_table()].
#' @param file If NULL, opens the output as temporary file. If a filename with path
#' is specified, saves the output to the specified path.
#' @param output The following output formats are available: excel and excel_nostyle.
#' @param print TRUE by default. If TRUE prints the output, if FALSE doesn't print anything. Can be used
#' if one only wants to catch the combined workbook.
#' @param monitor FALSE by default. If TRUE outputs two charts to visualize the functions time consumption.
#'
#' @return
#' A fully styled workbook containing the provided tables.
#'
#' @examples
#' # Example data frame
#' my_data <- dummy_data(1000)
#' my_data[["person"]] <- 1
#'
#' # Formats
#' age. <- discrete_format(
#'     "Total"          = 0:100,
#'     "under 18"       = 0:17,
#'     "18 to under 25" = 18:24,
#'     "25 to under 55" = 25:54,
#'     "55 to under 65" = 55:64,
#'     "65 and older"   = 65:100)
#'
#' sex. <- discrete_format(
#'     "Total"  = 1:2,
#'     "Male"   = 1,
#'     "Female" = 2)
#'
#' education. <- discrete_format(
#'     "Total"            = c("low", "middle", "high"),
#'     "low education"    = "low",
#'     "middle education" = "middle",
#'     "high education"   = "high")
#'
#' # Define style
#' set_style_options(column_widths = c(2, 15, 15, 15, 9))
#'
#' # Define titles and footnotes. If you want to add hyperlinks you can do so by
#' # adding "link:" followed by the hyperlink to the main text.
#' set_titles("This is title number 1 link: https://cran.r-project.org/",
#'            "This is title number 2 cell: W22",
#'            "This is title number 3 file: C:/MyFolder/MyFile.docx",
#'            "This is title number 4")
#' set_footnotes("This is footnote number 1 cell: W22",
#'               "This is footnote number 2 file: C:/MyFolder/MyFile.docx",
#'               "This is footnote number 3 link: https://cran.r-project.org/",
#'               "This is footnote number 4")
#'
#' # Catch the output and additionally use the options:
#' # print = FALSE and output = "excel_nostyle".
#' # This skips the styling and output part, so that the function runs faster.
#' # The styling is done later on.
#' set_print(FALSE)
#' set_output("excel_nostyle")
#' set_style_options(sheet_name = "big table")
#'
#' tab1 <- my_data |> any_table(rows       = c("sex + age", "sex", "age"),
#'                              columns    = c("year", "education + year"),
#'                              values     = weight,
#'                              statistics = c("sum", "pct_group"),
#'                              pct_group  = c("sex", "age", "education", "year"),
#'                              formats    = list(sex = sex., age = age.,
#'                                                education = education.),
#'                              na.rm      = TRUE)
#'
#' set_style_options(sheet_name = "age_sex")
#'
#' tab2 <- my_data |> any_table(rows       = "age",
#'                              columns    = "sex",
#'                              values     = weight,
#'                              statistics = "sum",
#'                              formats    = list(sex = sex., age = age.),
#'                              na.rm      = TRUE)
#'
#' set_style_options(sheet_name = "edu_year")
#'
#' tab3 <- my_data |> any_table(rows       = "education",
#'                              columns    = "year",
#'                              values     = weight,
#'                              statistics = "pct_group",
#'                              formats    = list(education = education.),
#'                              na.rm      = TRUE)
#'
#' # Every of the above tabs is a list, which contains the data table, an unstyled
#' # workbook and the meta information needed for the individual styling. These
#' # tabs can be input into the following function, which reads the meta information,
#' # styles each table individually and combines them as separate sheets into a single workbook.
#' combine_into_workbook(tab1, tab2, tab3)
#'
#' # Reset the global options afterwards
#' set_print(TRUE)
#' set_output("excel")
#'
#' @export
combine_into_workbook <- function(...,
                                  file    = NULL,
                                  output  = "excel",
                                  print   = TRUE,
                                  monitor = FALSE){
    #-------------------------------------------------------------------------#
    monitor_df <- NULL |> monitor_start("Prepare combine", "Prepare")
    #-------------------------------------------------------------------------#

    # Measure the time
    print_start_message()

    # Translate ... into a list if possible
    tables <- tryCatch({
        # Force evaluation to see if it exists
        list(...)
    }, error = function(e){
        # Evaluation failed
        NULL
    })

    if (is.null(tables)){
        print_message("ERROR", c("Unknown object found. Provide <any_table> or <export_with_style> results.",
								 "Combining tables to workbook will be aborted."))
        return(invisible(NULL))
    }

    for (table in tables){
        if (!is.list(table) || !all(c("table", "workbook", "meta") %in% names(table))){
            print_message("ERROR", c("Unknown object found. Provide <any_table> or <export_with_style> results.",
									 "Combining tables to workbook will be aborted."))
            return(invisible(NULL))
        }
    }

    tab_names <- as.character(substitute(list(...)))[-1]
    wb        <- openxlsx2::wb_workbook()

    i <- 1

    print_step("MAJOR", "Formatting tables")

    for (table in tables){
        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next(paste0("Format ", tab_names[i]), "Format tables")
        #---------------------------------------------------------------------#
        print_step("MINOR", "{tabs}", tabs = tab_names[i])

        meta <- table[["meta"]]

        wb <- wb |> prepare_styles(list("title" = meta[["titles"]], "footnote" = meta[["footnotes"]]), meta[["style"]])

        # Style data frame for export
        if (is.character(meta[[length(meta)]]) && meta[[length(meta)]] == "DATA"){
            wb_list <- suppressMessages(
                format_df_excel(wb, table[["table"]], meta[["titles"]], meta[["footnotes"]],
                                meta[["var_labels"]], meta[["style"]], meta[["column_align"]],
                                meta[["output"]], monitor_df))

            wb <- wb_list[[1]]
        }
        # Style any_table output for export
        else{
            # In case no by variables are provided
            if (length(meta[["by"]]) == 0){
                wb_list <- suppressMessages(
                    format_any_excel(wb, table[["table"]], meta[["rows"]], meta[["columns"]],
                                     meta[["statistics"]], meta[["by"]], meta[["titles"]],
                                     meta[["footnotes"]], meta[["var_labels"]], meta[["stat_labels"]],
                                     meta[["box"]], meta[["any_header"]],
                                     meta[["style"]], meta[["output"]], monitor_df = monitor_df))

                wb <- wb_list[[1]]
            }
            # In case there are by variables are provided
            else{
                wb_list <- suppressMessages(
                    format_any_by_excel(wb, table[["table"]], meta[["rows"]], meta[["columns"]],
                                        meta[["statistics"]], meta[["by"]], meta[["titles"]],
                                        meta[["footnotes"]], meta[["var_labels"]], meta[["stat_labels"]],
                                        meta[["box"]], meta[["any_header"]],
                                        meta[["style"]], meta[["output"]], meta[["na.rm"]], meta[["print_miss"]],
                                        monitor_df))

                wb <- wb_list[[1]]
            }
        }

        i <- i + 1
    }

    # Output formatted table into different formats
    if (print){
        print_step("MAJOR", "Exporting Excel Workbook")

        #---------------------------------------------------------------------#
        monitor_df <- monitor_df |> monitor_next("Output tables", "Output tables")
        #---------------------------------------------------------------------#

        if (is.null(file)){
            if (interactive()){
                wb$open()
            }
        }
        else if (!dir.exists(dirname(file)) || dirname(file) == "."){
            print_message("WARNING", "Directory '[file_name]' does not exist. File won't be saved.", file_name = dirname(file))

            if (interactive()){
                wb$open()
            }
        }
        else{
            wb$save(file = file, overwrite = TRUE)
        }
    }

    print_closing(10)

    #-------------------------------------------------------------------------#
    monitor_df <- monitor_df |> monitor_end()
    monitor_df |> monitor_plot(draw_plot = monitor)
    #-------------------------------------------------------------------------#

    invisible(wb)
}


###############################################################################
# Helper functions
###############################################################################
#' Make Space Between Plus Symbols Equal
#'
#' @description
#' Normalizes the space between plus symbols.
#'
#' @param combination Combination of variables connected with plus symbols.
#'
#' @return
#' Returns a normalized variable combination.
#'
#' @noRd
normalize_combination <- function(combination){
    # Remove extra spaces
    combination <- gsub("\\s*\\+\\s*", " + ", combination)
    combination <- gsub("\\s+", " ", combination)
    trimws(combination)
}


#' Expand Brackets To Generate Multiple Combinations
#'
#' @description
#' Expands variables passed in brackets to multiple full combinations.
#' "state + sex + (year age)" would become "state + sex + year" and
#' "state + sex + age".
#'
#' Can also expand combinations like "(state sex) + (year age)".
#'
#' @param combination Combination of variables connected with plus symbols.
#'
#' @return
#' Returns expanded variable combinations.
#'
#' @noRd
expand_combination <- function(combination){
    combination <- normalize_combination(combination)

    # Return combination as is, if there is no bracket to expand
    if (!grepl("\\(", combination)){
        return(stats::setNames(list(combination), combination))
    }

    # Find bracket groups
    matches <- gregexpr("\\([^\\)]+\\)", combination)
    groups  <- regmatches(combination, matches)[[1]]

    # Extract single variables
    variable_list <- lapply(groups, function(group){
        # Remove brackets
        content <- gsub("[()]", "", group)

        # Split only on commas
        terms <- strsplit(content, ",", fixed = TRUE)[[1]]

        # Trim whitespace around each alternative
        trimws(terms)
    })

    # Cartesian combinations
    combinations <- expand.grid(variable_list, stringsAsFactors = FALSE)

    # Create combination template to insert new combinations into
    template <- combination

    for (i in seq_along(groups)){
        template <- sub("\\([^\\)]+\\)", "%s", template)
    }

    # Put together the final expanded variable combinations
    results <- apply(combinations, 1, function(row){
        normalize_combination(do.call(sprintf, c(template, as.list(row))))
    })

    # mapping
    stats::setNames(as.list(rep(combination, length(results))), results)
}


#' Expand Brackets To Generate Multiple Combinations
#'
#' @description
#' Expands variables passed in brackets to multiple full combinations.
#' "state + sex + (year age)" would become "state + sex + year" and
#' "state + sex + age"
#'
#' @param combinations Combination vector of variables connected with plus symbols.
#'
#' @return
#' Returns expanded variable combination list.
#'
#' @noRd
expand_brackets <- function(combinations){
    # Expand all combinations
    result <- unlist(lapply(combinations, expand_combination), recursive = FALSE)

    # Remove duplicate combinations
    names <- result[collapse::funique(names(result))]
    list(names, any(grepl("\\(", unlist(names))))
}
