###############################################################################
# Conversion
###############################################################################

#' Converts Numbers into 'Excel' Ranges
#'
#' @description
#' Converts a column number into the according letter to form a cell reference
#' like it is used in 'Excel' (e.g "A1"). Also can compute a range from cell to
#' cell (e.g. "A1:BY22").
#'
#' @param row Single row number.
#' @param column Single column number.
#' @param from_row Range start row.
#' @param from_column Range start column.
#' @param to_row Range end row.
#' @param to_column Range end column.
#'
#' @return
#' Returns a character with an 'Excel' range.
#'
#' @examples
#' single_cell <- get_excel_range(row = 1, column = 6)
#' range       <- get_excel_range(from_row = 1, from_column = 6,
#'                                  to_row = 5,   to_column = 35)
#'
#' @export
get_excel_range <- function(row      = NULL, column      = NULL,
                            from_row = NULL, from_column = NULL,
                            to_row   = NULL, to_column   = NULL){

    # Get single cell
    if (!is.null(row) && !is.null(column)){
        if (row <= 0 || column <= 0){
            print_message("ERROR", "Row and column must be greater than 0.")
            return(NULL)
        }

        return(openxlsx2::wb_dims(rows = row, cols = column))
    }

    # Else get cell range
    if (!is.null(from_row) && !is.null(from_column) &&
        !is.null(to_row) && !is.null(to_column)){
            if (from_column <= 0 || from_row <= 0 || to_row <= 0 || to_column <= 0){
                # No error message here because any_table runs into this regularly if
                # e.g. there are no titles set.
                return(NULL)
            }

        return(openxlsx2::wb_dims(rows = seq.int(from_row, to_row),
                                  cols = seq.int(from_column, to_column)))
    }

    NULL
}

###############################################################################
# Getting table parts for any_table
###############################################################################
#' Get Different Table Parts as Excel Ranges
#'
#' @description
#' Identifies the different table parts (like titles, header, inner table cells)
#' and returns their Ranges in Excel format to be used by openxlsx2.
#'
#' @param table The data frame which holds the table information.
#' @param multi_header The multi layered column header produced by any_table.
#' @param titles Titles if there are any.
#' @param footnotes Footnotes if there are any.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a list with table ranges.
#'
#' @noRd
get_any_table_ranges <- function(table,
                                 multi_header,
                                 stats_row,
                                 titles     = NULL,
                                 footnotes  = NULL,
                                 style      = excel_output_style()){
    # If titles are provided put them in the starting row
    if (!is.null(titles)){
        title.row     <- style[["start_row"]]
        title.column  <- style[["start_column"]]
        title.length  <- length(titles)

        header.row    <- title.row + length(titles) + 1
        header.column <- title.column
    }
    # If no titles are provided the table headers are in the starting row
    else{
        title.row     <- 0
        title.column  <- 0
        title.length  <- 0

        header.row    <- style[["start_row"]]
        header.column <- style[["start_column"]]
    }

    # Get the basic starting cells, as well as table dimensions
    header.width  <- collapse::fncol(multi_header)
    header.length <- collapse::fnrow(multi_header)

    table.row     <- header.row + header.length
    table.length  <- collapse::fnrow(table)
    table.width   <- collapse::fncol(table)
    table.end     <- header.column + table.width - 1
    cat_col.width <- collapse::fncol(table) - collapse::fncol(multi_header)

    footnote.row <- table.row + table.length + 1

    if (!is.null(footnotes)){
        footnote.length <- length(footnotes)
    }
    else{
        footnote.length <- 0
    }

    # Individually compute cell ranges in Excel format
    title_range <- get_excel_range(from_row  = title.row, from_column = title.column,
                                   to_row    = title.row + (title.length - 1),
                                   to_column = title.column)

    whole_tab_range <- get_excel_range(from_row  = header.row, from_column = header.column,
                                       to_row    = table.row     + table.length - 1,
                                       to_column = header.column + (cat_col.width - 1) + header.width)

    header_range <- get_excel_range(from_row  = header.row, from_column = header.column,
                                    to_row    = header.row    + (header.length - 1),
                                    to_column = header.column + (cat_col.width - 1) + header.width)

    box_range <- get_excel_range(from_row  = header.row, from_column = header.column,
                                 to_row    = header.row    + (header.length - 1),
                                 to_column = header.column + (cat_col.width - 1))

    cat_col_range <- get_excel_range(from_row  = table.row, from_column = header.column,
                                     to_row    = table.row     + (table.length - 1),
                                     to_column = header.column + (cat_col.width - 1))

    table_range <- get_excel_range(from_row    = table.row,
                                   from_column = header.column + cat_col.width,
                                   to_row      = table.row     + (table.length - 1),
                                   to_column   = header.column + (cat_col.width - 1) + header.width)

    footnote_range <- get_excel_range(from_row    = footnote.row,
                                      from_column = header.column,
                                      to_row      = footnote.row  + (footnote.length - 1),
                                      to_column   = header.column)

    # Get subheader ranges, if present
    if ("subheaders" %in% names(style)){
        subheader_range     <- list()
        pre_subheader_range <- list()
        subheader_rows      <- style[["subheader_rows"]][style[["subheader_rows"]] != 0]

        # Loop through all subheader rows and extract excel ranges
        for(row in seq_along(subheader_rows)){
            current_row <- subheader_rows[[row]]

            subheader_range[[row]] <- get_excel_range(from_row    = table.row + (current_row - 1),
                                                      from_column = header.column + cat_col.width,
                                                      to_row      = table.row + (current_row - 1),
                                                      to_column   = header.column + (cat_col.width - 1) + header.width)

            pre_subheader_range[[row]] <- get_excel_range(from_row    = table.row + (current_row - 1),
                                                          from_column = header.column,
                                                          to_row      = table.row + (current_row - 1),
                                                          to_column   = header.column + (cat_col.width - 1))
        }
    }

    # Output ranges for further use
    all    <- as.list(environment())
    ranges <- names(formals(sys.function()))

    all[setdiff(names(all), ranges)]
}


#' Get All The Table Ranges of Any Table as Excel Ranges
#'
#' @description
#' Identifies the different table ranges and returns them as ranges in Excel format
#' to be used by 'openxlsx2'.
#'
#' @param any_tab The data frame for any table.
#' @param multi_header The multi layered column header produced by any_table.
#' @param col_header_dimensions A list containing the column variable names and the
#' column header dimensions.
#' @param row_header_dimensions A list containing the row variable names and the
#' row header dimensions.
#' @param titles Titles if there are any.
#' @param footnotes Footnotes if there are any.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a list with table ranges.
#'
#' @noRd
get_any_tab_ranges <- function(any_tab,
                               multi_header,
                               stats_row,
                               titles     = NULL,
                               footnotes  = NULL,
                               style      = excel_output_style()){
    # Get basic table ranges
    table_ranges <- get_any_table_ranges(any_tab, multi_header, stats_row,
                                         titles, footnotes, style)

    # Get specific parts of the table
    any_col_ranges <- list()

    chunks        <- rle(stats_row)
    chunk_lengths <- chunks[["lengths"]]

    col_start <- table_ranges[["header.column"]] + table_ranges[["cat_col.width"]]

    # Get the more detailed blocks
    if (style[["header_stat_merging"]] == "block"){
        start_var <- (collapse::fncol(any_tab) - collapse::fncol(multi_header)) + 1

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

        any_col_ranges[["block_lengths"]] <- rle(root_names)[["lengths"]]

        # Get the statistic symbols or texts to print
        any_col_ranges[["block_values"]] <- c()
        current_column <- 0

        for (block_length in any_col_ranges[["block_lengths"]]){
            current_column <- current_column + block_length

            any_col_ranges[["block_values"]] <- c(any_col_ranges[["block_values"]],
                                                  multi_header[collapse::fnrow(multi_header), current_column])
        }
    }

    # Get the wider blocks per statistic
    for (i in seq_along(chunk_lengths)){
        from_col <- col_start + sum(chunk_lengths[seq_len(i - 1)])
        to_col   <- col_start + sum(chunk_lengths[seq_len(i)])

        range_name <- paste0("any_col_ranges", i)

        any_col_ranges[[range_name]] <-
                get_excel_range(from_row    = table_ranges[["table.row"]],
                                from_column = from_col,
                                to_row      = table_ranges[["table.row"]] +
                                             (table_ranges[["table.length"]] - 1),
                                to_column   = to_col - 1)
    }

    # Output ranges for further use
    c(table_ranges, any_col_ranges)
}


#' Merge Column Header Labels With The Same Name
#'
#' @description
#' If columns have the same name or if there are empty header cells, merge them
#' together to enhance the visual appeal of the table header.
#'
#' @param wb The currently processed workbook.
#' @param column_header The multi layered column header.
#' @param ranges Table ranges.
#'
#' @return
#' Returns a workbook with merged column headers.
#'
#' @noRd
handle_col_header_merge <- function(wb, column_header, ranges, style){
    # Get all values in order of appearance with their respective lengths
    row_values <- lapply(seq_len(collapse::fnrow(column_header)), function(row){
        rle(as.character(column_header[row, ]))
    })

    # Merges the statistic labels block wise. E.g. if multiple percentages are side
    # by side, then the individual percentage blocks get merged, so that one can still
    # tell them apart.
    if (style[["header_stat_merging"]] == "block"){
        row <- length(row_values)
        row_values[[row]] <- list(lengths = ranges[["block_lengths"]],
                                  values  = ranges[["block_values"]])
    }
    # No merging, every statistic is written over every column. This is identical
    # behavior in comparison to SAS.
    else if (style[["header_stat_merging"]] == "none"){
        row <- length(row_values)
        row_values[[row]] <- list(lengths = rep(1, collapse::fncol(column_header)),
                                  values  = as.character(column_header[row, ]))
    }

    # Define offsets in Excel table
    row_offset <- ranges[["header.row"]] - 1
    col_offset <- ranges[["header.column"]] + (ranges[["cat_col.width"]] - 1)

    # Loop through all rows
    number_of_rows <- length(row_values)
    merge_dims     <- character()

    for (row in seq_along(row_values)){
        current_row <- row_values[[row]]
        start_col <- 1

        # Loop through all column chunks
        for (i in seq_along(current_row[["values"]])){
            value <- current_row[["values"]][i]
            space <- current_row[["lengths"]][i]

            # If no empty value and there is an actual need for merging because more
            # than one column has the same value
            if (value != ""){
                from_row <- row
                from_col <- start_col
                to_row   <- row
                to_col   <- start_col + (space - 1)

                # Expand down if upcoming rows are empty in the same part
                if (row < number_of_rows){
                    for (next_row in (row + 1):length(row_values)){
                        slice <- as.character(column_header[next_row, from_col:to_col])

                        # If upcoming row in the same part is empty, expand cell merging
                        # further down
                        if (all(slice == "")){
                            to_row <- next_row
                        }
                        # If row part is not empty exit loop. No further expand of
                        # cell merging.
                        else{
                            break
                        }
                    }
                }

                # Merge cells
                if (from_row == to_row && from_col == to_col){
                    start_col <- start_col + space
                    next
                }

                # Collect all the dims that need to be merged in a vector
                merge_dims <- c(merge_dims, get_excel_range(from_row    = row_offset + from_row,
                                                            from_column = col_offset + from_col,
                                                            to_row      = row_offset + to_row,
                                                            to_column   = col_offset + to_col))
            }

            start_col <- start_col + max(1, space)
        }
    }

    # Merge all dims in one go
    wb$merge_cells(dims = merge_dims)

    wb
}


#' Merge Row Header Labels With The Same Name
#'
#' @description
#' If rows have the same name or if there are empty header cells, merge them
#' together to enhance the visual appeal of the table header.
#'
#' @param wb The currently processed workbook.
#' @param row_header The multi layered row header.
#' @param ranges Table ranges.
#'
#' @return
#' Returns a workbook with merged column headers.
#'
#' @noRd
handle_row_header_merge <- function(wb, row_header, ranges){
    # Make sure row_header is treated as a matrix, even though there can be only one column
    if (is.null(dim(row_header))){
        dim(row_header) <- c(length(row_header), 1)
    }

    # Get all values in order of appearance with their respective lengths (per column)
    col_values <- lapply(seq_len(collapse::fncol(row_header)), function(column){
        rle(as.character(row_header[, column]))
    })

    # Define offsets in Excel table
    row_offset <- ranges[["table.row"]] - 1
    col_offset <- ranges[["header.column"]] - 1

    # Loop through all columns
    number_of_columns <- length(col_values)
    merge_dims <- character()

    for (column in seq_along(col_values)){
        current_col <- col_values[[column]]
        start_row   <- 1

        # Loop through all row chunks
        for (i in seq_along(current_col[["values"]])){
            value <- current_col[["values"]][i]
            space <- current_col[["lengths"]][i]

            # If no empty value and there is an actual need for merging because more
            # than one column has the same value
            if (value != ""){
                from_col <- column
                from_row <- start_row
                to_col   <- column
                to_row   <- start_row + (space - 1)

                # Expand right if upcoming columns are empty in the same block
                if (column < number_of_columns){
                    for (next_column in seq.int(column + 1, number_of_columns)){
                        slice <- as.character(row_header[from_row:to_row, next_column])

                        # If upcoming column in the same part is empty, expand cell merging
                        # further right
                        if (all(slice == "")){
                            to_col <- next_column
                        }
                        # If column part is not empty exit loop. No further expand of
                        # cell merging.
                        else{
                            break
                        }
                    }
                }

                # Merge cells
                if (from_row == to_row && from_col == to_col){
                    start_row <- start_row + space
                    next
                }

                # Collect all the dims that need to be merged in a vector
                merge_dims <- c(merge_dims, get_excel_range(from_row    = row_offset + from_row,
                                                            from_column = col_offset + from_col,
                                                            to_row      = row_offset + to_row,
                                                            to_column   = col_offset + to_col))
            }

            start_row <- start_row + space
        }
    }

    # Merge all dims in one go
    wb$merge_cells(dims = merge_dims)

    wb
}


###############################################################################
# Get ranges for export with style
###############################################################################
#' Get Data Frame Ranges
#'
#' @description
#' Identifies the different table ranges of a data frame and returns them as ranges
#' in Excel format to be used by 'openxlsx2'.
#'
#' @param data_frame The data frame to get the ranges from.
#' @param titles Titles if there are any.
#' @param footnotes Footnotes if there are any.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a list with table ranges.
#'
#' @noRd
get_df_ranges <- function(data_frame,
                          titles     = NULL,
                          footnotes  = NULL,
                          style      = excel_output_style()){
    # Get basic table ranges
    table_ranges <- get_table_ranges(data_frame, titles, footnotes, style)

    # Get specific parts of mean table
    df_col_ranges <- list()
    df_col_types  <- list()

    col_start <- table_ranges[["header.column"]] - 1

    extensions <- c("sum", "pct", "group", "total", "value", "freq",
                    "g0", "mean", "median", "mode", "min", "max", "first",
                    "last", "wgt", "p", "sd", "variance", "missing")

    format_index <- 1

    for (i in seq_len(collapse::fncol(data_frame))){
        # If a variable doesn't have a statistics extension, it is likely not
        # a variable that needs a number format.
        var_end <- sub("p[0-9]+$", "p", utils::tail(strsplit(names(data_frame)[[i]], "_")[[1]], 1))

        if (!var_end %in% extensions){
            next
        }

        # Add number format
        col_to_format <- col_start + i

        range_name <- paste0("df_col_ranges", format_index)
        type_name  <- paste0("df_col_types", format_index)

        df_col_ranges[[range_name]] <-
            get_excel_range(from_row    = table_ranges[["table.row"]],
                            from_column = col_to_format,
                            to_row      = table_ranges[["table.row"]] +
                                (table_ranges[["table.length"]] - 1),
                            to_column   = col_to_format)

        df_col_types[[type_name]] <- var_end

        format_index <- format_index + 1
    }

    table_ranges[["num_format.length"]] <- format_index - 1

    # Output ranges for further use
    c(table_ranges, df_col_ranges, df_col_types)
}


###############################################################################
# Getting table parts
###############################################################################
#' Get Different Table Parts as Excel Ranges
#'
#' @description
#' Identifies the different table parts (like titles, header, inner table cells)
#' and returns their Ranges in Excel format to be used by openxlsx2.
#'
#' @param table The data frame which holds the table information.
#' @param titles Titles if there are any.
#' @param footnotes Footnotes if there are any.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a list with table ranges.
#'
#' @noRd
get_table_ranges <- function(table,
                             titles     = NULL,
                             footnotes  = NULL,
                             style      = excel_output_style()){
    # If titles are provided put them in the starting row
    if (!is.null(titles)){
        title.row     <- style[["start_row"]]
        title.column  <- style[["start_column"]]
        title.length  <- length(titles)

        header.row    <- title.row + length(titles) + 1
        header.column <- title.column
    }
    # If no titles are provided the table headers are in the starting row
    else{
        title.row     <- 0
        title.column  <- 0
        title.length  <- 0

        header.row    <- style[["start_row"]]
        header.column <- style[["start_column"]]
    }

    # Get the basic starting cells, as well as table dimensions
    header.width  <- collapse::fncol(table) - 1

    table.row     <- header.row + 1
    table.length  <- collapse::fnrow(table)
    table.width   <- collapse::fncol(table)
    table.end     <- header.column + table.width - 1
    cat_col.width <- 1

    footnote.row <- table.row + table.length + 1

    if (!is.null(footnotes)){
        footnote.length <- length(footnotes)
    }
    else{
        footnote.length <- 0
    }

    # Individually compute cell ranges in Excel format
    title_range <- get_excel_range(from_row  = title.row, from_column = title.column,
                                   to_row    = title.row + (title.length - 1),
                                   to_column = title.column)

    whole_tab_range <- get_excel_range(from_row  = header.row, from_column = header.column,
                                       to_row    = header.row    + table.length,
                                       to_column = header.column + header.width)

    header_range <- get_excel_range(from_row  = header.row, from_column = header.column,
                                    to_row    = header.row,
                                    to_column = header.column + header.width)

    box_range <- get_excel_range(from_row  = header.row, from_column = header.column,
                                 to_row    = header.row,
                                 to_column = header.column)

    cat_col_range <- get_excel_range(from_row  = table.row, from_column = header.column,
                                     to_row    = header.row    + table.length,
                                     to_column = header.column)

    table_range <- get_excel_range(from_row    = table.row,
                                   from_column = header.column + 1,
                                   to_row      = header.row    + table.length,
                                   to_column   = header.column + header.width)

    footnote_range <- get_excel_range(from_row    = footnote.row,
                                      from_column = header.column,
                                      to_row      = footnote.row  + (footnote.length - 1),
                                      to_column   = header.column)

    # Output ranges for further use
    all    <- as.list(environment())
    ranges <- names(formals(sys.function()))

    all[setdiff(names(all), ranges)]
}


#' Get Columns of Mean Table as Excel Ranges
#'
#' @description
#' Identifies the different table columns and returns their ranges in Excel format
#' to be used by 'openxlsx2'.
#'
#' @param mean_tab The data frame for the mean table produced by frequencies.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a list with table ranges.
#'
#' @noRd
get_mean_tab_ranges <- function(mean_tab,
                                style = excel_output_style()){
    # Get basic table ranges
    table_ranges <- get_table_ranges(mean_tab, style = style)

    # Get specific parts of mean table
    mean_col_ranges <- c()

    for (column in 1:6){
        mean_col_ranges <- c(mean_col_ranges,
                             get_excel_range(from_row    = table_ranges[["header.row"]] + 1,
                                             from_column = table_ranges[["header.column"]] + column,
                                             to_row      = table_ranges[["header.row"]] + table_ranges[["table.length"]],
                                             to_column   = table_ranges[["header.column"]] + column))
    }

    # Output ranges for further use
    c(table_ranges,
      mean_col_ranges = mean_col_ranges)
}


#' Get Columns of Freq Table as Excel Ranges
#'
#' @description
#' Identifies the different table columns and returns their ranges in Excel format
#' to be used by 'openxlsx2'.
#'
#' @param freq_tab The data frame for the freq table produced by frequencies.
#' @param titles Titles if there are any.
#' @param footnotes Footnotes if there are any.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a list with table ranges.
#'
#' @noRd
get_freq_tab_ranges <- function(freq_tab,
                                titles     = NULL,
                                footnotes  = NULL,
                                style      = excel_output_style()){
    # Get basic table ranges
    table_ranges <- get_table_ranges(freq_tab, titles, footnotes, style)

    # Get specific parts of mean table
    freq_col_ranges <- c()

    for (column in 1:6){
        freq_col_ranges <- c(freq_col_ranges,
                             get_excel_range(from_row    = table_ranges[["header.row"]] + 1,
                                             from_column = table_ranges[["header.column"]] + column,
                                             to_row      = table_ranges[["header.row"]] + table_ranges[["table.length"]],
                                             to_column   = table_ranges[["header.column"]] + column))
    }

    # Output ranges for further use
    c(table_ranges,
      freq_col_ranges = freq_col_ranges)
}

###############################################################################
# Format titles and footnotes if there are any
###############################################################################
#' Format Titles and Footnotes
#'
#' @description
#' Format titles and footnotes if there are any.
#'
#' @param wb The workbook to modify.
#' @param ranges Table ranges to format.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a further formatted workbook.
#'
#' @noRd
format_titles_foot_excel <- function(wb, titles, footnotes, ranges, style, output){
    if (output == "excel"){
        pattern <- "(link|cell|file):"

        if (length(titles) > 0){
            # Format
            if (length(style[["title_font_color"]]) == 1){
                for (i in seq_len(length(style[["title_alignment"]]))){
                    # Apply cell styles
                    wb$add_cell_style(dims       = ranges[["title_range"]],
                                      horizontal = style[["title_alignment"]][i],
                                      vertical   = "center",
                                      wrap_text  = "1",
                                      apply_font = TRUE,
                                      font_id    = wb$styles_mgr$get_font_id("title_font"))
                }
            }
            else{
                # Apply cell styles
                full_range <- openxlsx2::dims_to_rowcol(ranges[["title_range"]])

                # Adjust alignment vector to fit the rest
                style[["title_alignment"]] <- fill_or_trim(style[["title_alignment"]],
                                                           length(titles))

                for (i in seq_along(titles)){
                    specific_range <- openxlsx2::wb_dims(as.integer(full_range[["row"]][i]),
                                                         full_range[["col"]])

                    wb$add_cell_style(dims       = specific_range,
                                      horizontal = style[["title_alignment"]][i],
                                      vertical   = "center",
                                      wrap_text  = "1",
                                      apply_font = TRUE,
                                      font_id    = wb$styles_mgr$get_font_id(paste0("title", i, "_font")))
                }
            }

            # Merge the titles over the span of the table
            for (title in seq_along(titles)){
                wb$merge_cells(dims = openxlsx2::wb_dims(
                    cols = style[["start_column"]]:ranges[["table.end"]],
                    rows = style[["start_row"]] + (title - 1)))
            }

            # Set row heights for titles if specified
            title_heights <- style[["title_heights"]]

            if (!is.null(title_heights)){
                number_of_rows <- length(titles)
                start_row      <- ranges[["title.row"]]
                end_row        <- start_row + number_of_rows - 1

                title_heights <- fill_or_trim(title_heights,
                                              number_of_rows)

                wb$set_row_heights(rows    = start_row:end_row,
                                   heights = title_heights)
            }

            # Shortcut: if no links found, just add all titles at once
            if (!any(grepl(pattern, titles))){
                wb$add_data(x         = titles,
                            start_col = style[["start_column"]],
                            start_row = style[["start_row"]])
            }
            # Otherwise print title by title
            else{
                # Adjust alignment vector to fit the rest
                style[["title_alignment"]] <- fill_or_trim(style[["title_alignment"]],
                                                           length(titles))

                for (i in seq_along(titles)){
                    title <- titles[[i]]

                    # If there is a hyperlink in the text
                    if (grepl(pattern, title)){
                        # Identify which of the patterns was found
                        matched_pattern <- regexec(pattern, title)
                        found_pattern   <- regmatches(title, matched_pattern)[[1]][2]

                        # Get the link and the title range
                        current_link <- sub(paste0(".*", pattern), "", title)
                        current_pos  <- get_excel_range(row    = ranges[["title.row"]] + (i - 1),
                                                        column = ranges[["title.column"]])

                        # Remove link from the text
                        titles[[i]] <- sub(paste0(pattern, ".*"), "", title)

                        # Add hyperlink
                        if (found_pattern == "link"){
                            formula <- openxlsx2::create_hyperlink(text = titles[[i]], file = current_link)

                            wb$add_formula(x = formula, dims = current_pos)
                        }
                        # Add link to cell
                        else if (found_pattern == "cell"){
                            formula <- paste0('=HYPERLINK(\"#', current_link, '\", "', titles[[i]], '")')

                            wb$add_formula(x = formula, dims = current_pos)
                        }
                        # Add file link
                        else if (found_pattern == "file"){
                            formula <- paste0('=HYPERLINK("[', current_link, ']", "', titles[[i]], '")')

                            wb$add_formula(x = formula, dims = current_pos)
                        }

                        if (length(style[["title_font_color"]]) == 1){
                            wb$add_cell_style(dims       = current_pos,
                                              horizontal = style[["title_alignment"]][i],
                                              vertical   = "center",
                                              wrap_text  = "1",
                                              apply_font = TRUE,
                                              font_id    = wb$styles_mgr$get_font_id("title_link_font"))
                        }
                        else{
                            wb$add_cell_style(dims       = current_pos,
                                              horizontal = style[["title_alignment"]][i],
                                              vertical   = "center",
                                              wrap_text  = "1",
                                              apply_font = TRUE,
                                              font_id    = wb$styles_mgr$get_font_id(paste0("title", i, "_link_font")))
                        }
                    }
                    # Without links just insert plain text
                    else{
                        current_pos <- get_excel_range(row    = ranges[["title.row"]] + (i - 1),
                                                       column = ranges[["title.column"]])

                        wb$add_data(x = title, dims = current_pos)
                    }
                }
            }
        }

        # Format footnotes if there are any
        if (length(footnotes) > 0){
            # Format
            if (length(style[["footnote_font_color"]]) == 1){
                for (i in seq_len(length(style[["footnote_alignment"]]))){
                    # Apply cell styles
                    wb$add_cell_style(dims       = ranges[["footnote_range"]],
                                      horizontal = style[["footnote_alignment"]][i],
                                      vertical   = "center",
                                      wrap_text  = "1",
                                      apply_font = TRUE,
                                      font_id    = wb$styles_mgr$get_font_id("footnote_font"))
                }

                # Format first footnote row special with a separating line
                full_range    <- openxlsx2::dims_to_rowcol(ranges[["footnote_range"]])
                cat_col_range <- openxlsx2::dims_to_rowcol(ranges[["cat_col_range"]])

                first_foot <- openxlsx2::wb_dims(ranges[["footnote.row"]],
                                                 cat_col_range[["col"]])

                wb$add_cell_style(dims         = first_foot,
                                  horizontal   = style[["footnote_alignment"]][1],
                                  vertical     = "center",
                                  wrap_text    = "1",
                                  apply_font   = TRUE,
                                  font_id      = wb$styles_mgr$get_font_id("footnote_font"),
                                  apply_border = TRUE,
                                  border_id    = wb$styles_mgr$get_border_id("footnote_borders"))
            }
            else{
                # Apply cell styles
                full_range    <- openxlsx2::dims_to_rowcol(ranges[["footnote_range"]])
                cat_col_range <- openxlsx2::dims_to_rowcol(ranges[["cat_col_range"]])

                # Adjust alignment vector to fit the rest
                style[["footnote_alignment"]] <- fill_or_trim(style[["footnote_alignment"]],
                                                           length(footnotes))

                for (i in seq_along(footnotes)){
                    if (i > 1){
                        specific_range <- openxlsx2::wb_dims(as.integer(full_range[["row"]][i]),
                                                             full_range[["col"]])

                        wb$add_cell_style(dims       = specific_range,
                                          horizontal = style[["footnote_alignment"]][i],
                                          vertical   = "center",
                                          wrap_text  = "1",
                                          apply_font = TRUE,
                                          font_id    = wb$styles_mgr$get_font_id(paste0("footnote", i, "_font")))
                    }
                    # Format first footnote row special with a separating line
                    else{
                        specific_range <- openxlsx2::wb_dims(as.integer(full_range[["row"]][i]),
                                                             cat_col_range[["col"]])

                        wb$add_cell_style(dims       = specific_range,
                                          horizontal = style[["footnote_alignment"]][i],
                                          vertical   = "center",
                                          wrap_text  = "1",
                                          apply_font = TRUE,
                                          font_id    = wb$styles_mgr$get_font_id(paste0("footnote", i, "_font")),
                                          apply_border = TRUE,
                                          border_id    = wb$styles_mgr$get_border_id("footnote_borders"))
                    }
                }
            }

            # Merge the footnotes over the span of the table
            for (footnote in seq_along(footnotes)){
                wb$merge_cells(dims = openxlsx2::wb_dims(
                    cols = style[["start_column"]]:ranges[["table.end"]],
                    rows = ranges[["footnote.row"]] + (footnote - 1)))
            }

            # Set row heights for footnotes if specified
            footnote_heights <- style[["footnote_heights"]]

            if (!is.null(footnote_heights)){
                number_of_rows   <- length(footnotes)
                start_row        <- ranges[["footnote.row"]]
                end_row          <- start_row + number_of_rows - 1

                footnote_heights <- fill_or_trim(footnote_heights,
                                                 number_of_rows)

                wb$set_row_heights(rows    = start_row:end_row,
                                   heights = footnote_heights)
            }

            # Shortcut: if no links found, just add all titles at once
            if (!any(grepl(pattern, footnotes))){
                wb$add_data(x         = footnotes,
                            start_col = style[["start_column"]],
                            start_row = ranges[["footnote.row"]])
            }
            # Otherwise print footnote by footnote
            else{
                # Adjust alignment vector to fit the rest
                style[["footnote_alignment"]] <- fill_or_trim(style[["footnote_alignment"]],
                                                              length(footnotes))

                for (i in seq_along(footnotes)){
                    footnote <- footnotes[[i]]

                    # If there is a hyperlink in the text
                    if (grepl(pattern, footnote)){
                        # Identify which of the patterns was found
                        matched_pattern <- regexec(pattern, footnote)
                        found_pattern   <- regmatches(footnote, matched_pattern)[[1]][2]

                        # Get the link and the footnote range
                        current_link <- sub(paste0(".*", pattern), "", footnote)
                        current_pos  <- get_excel_range(row    = ranges[["footnote.row"]] + (i - 1),
                                                        column = ranges[["title.column"]])

                        # Remove link from the text
                        footnote[[i]] <- sub(paste0(pattern, ".*"), "", footnote)

                        # Add hyperlink
                        if (found_pattern == "link"){
                            formula <- openxlsx2::create_hyperlink(text = footnote[[i]], file = current_link)

                            wb$add_formula(x = formula, dims = current_pos)
                        }
                        # Add link to cell
                        else if (found_pattern == "cell"){
                            formula <- paste0('=HYPERLINK(\"#', current_link, '\", "', footnote[[i]], '")')

                            wb$add_formula(x = formula, dims = current_pos)
                        }
                        # Add file link
                        else if (found_pattern == "file"){
                            formula <- paste0('=HYPERLINK("[', current_link, ']", "', footnote[[i]], '")')

                            wb$add_formula(x = formula, dims = current_pos)
                        }

                        if (length(style[["footnote_font_color"]]) == 1){
                            wb$add_cell_style(dims       = current_pos,
                                              horizontal = style[["footnote_alignment"]][i],
                                              vertical   = "center",
                                              wrap_text  = "1",
                                              apply_font = TRUE,
                                              font_id    = wb$styles_mgr$get_font_id("footnote_link_font"))
                        }
                        else{
                            wb$add_cell_style(dims       = current_pos,
                                              horizontal = style[["footnote_alignment"]][i],
                                              vertical   = "center",
                                              wrap_text  = "1",
                                              apply_font = TRUE,
                                              font_id    = wb$styles_mgr$get_font_id(paste0("footnote", i, "_link_font")))
                        }
                    }
                    # Without links just insert plain text
                    else{
                        current_pos <- get_excel_range(row    = ranges[["footnote.row"]] + (i - 1),
                                                       column = ranges[["title.column"]])

                        wb$add_data(x = footnote, dims = current_pos)
                    }
                }
            }
        }
    }
    # If not a styled export just print titles and footnotes as they are
    else{
        if (length(titles) > 0){
            wb$add_data(x         = titles,
                        start_col = style[["start_column"]],
                        start_row = style[["start_row"]])
        }

        if (length(footnotes) > 0){
            wb$add_data(x         = footnotes,
                        start_col = style[["start_column"]],
                        start_row = ranges[["footnote.row"]])
        }
    }

    wb
}

###############################################################################
# Handle style elements
###############################################################################
#' Handle Cell Styles
#'
#' @description
#' Apply cell styles to a range with all style elements in one go.
#'
#' @param wb The workbook to modify.
#' @param ranges Table ranges to format.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a further formatted workbook.
#'
#' @noRd
handle_cell_styles <- function(wb,
                               ranges,
                               style = excel_output_style()){
    # Apply individual styles for each table part
    for (type in c("header", "box", "cat_col", "table", "subheader")){

        apply_font <- NULL
        font_id    <- NULL
        if (paste0(type, "_font") %in% wb$styles_mgr$font$name){
            apply_font <- TRUE
            font_id    <- wb$styles_mgr$get_font_id(paste0(type, "_font"))
        }

        apply_border <- NULL
        border_id    <- NULL
        if (paste0(type, "_borders") %in% wb$styles_mgr$border$name){
            apply_border <- TRUE
            border_id    <- wb$styles_mgr$get_border_id(paste0(type, "_borders"))
        }

        apply_fill <- NULL
        fill_id    <- NULL
        if (paste0(type, "_fill") %in% wb$styles_mgr$fill$name){
            apply_fill <- TRUE
            fill_id    <- wb$styles_mgr$get_fill_id(paste0(type, "_fill"))
        }

        # Subheader ranges are stored inside a list which has to be looped over
        if (type == "subheader"){
            for (sub_range in seq_along(ranges[["subheader_range"]])){
                wb$add_cell_style(dims         = ranges[["subheader_range"]][[sub_range]],
                                  horizontal   = style[[paste0(type, "_alignment")]],
                                  vertical     = "center",
                                  wrap_text    = style[[paste0(type, "_wrap")]],
                                  indent       = style[[paste0(type, "_indent")]],
                                  apply_font   = apply_font,
                                  font_id      = font_id,
                                  apply_border = apply_border,
                                  border_id    = border_id,
                                  apply_fill   = apply_fill,
                                  fill_id      = fill_id)
            }
        }
        # All other ranges are stored as single entries
        else{
            wb$add_cell_style(dims         = ranges[[paste0(type, "_range")]],
                              horizontal   = style[[paste0(type, "_alignment")]],
                              vertical     = "center",
                              wrap_text    = style[[paste0(type, "_wrap")]],
                              indent       = style[[paste0(type, "_indent")]],
                              apply_font   = apply_font,
                              font_id      = font_id,
                              apply_border = apply_border,
                              border_id    = border_id,
                              apply_fill   = apply_fill,
                              fill_id      = fill_id)
        }
    }

    wb
}


#' Set Up Different Fill Styles
#'
#' @description
#' Pass fill styles for the different parts of the table into the style manager
#' to apply them more efficiently later.
#'
#' @param wb The workbook to modify.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a workbook with added style elements.
#'
#' @noRd
handle_fill_styles <- function(wb, style = excel_output_style()){
    # Add individual fill styles for each table part
    for (type in c("header", "subheader", "box", "cat_col", "table")){
        # With color fill
        if (style[[paste0(type, "_back_color")]] != ""){
            wb$styles_mgr$add(
                openxlsx2::create_fill(pattern_type = "solid",
                                       fg_color     = openxlsx2::wb_color(style[[paste0(type, "_back_color")]])),
                paste0(type, "_fill"))
        }
        # With transparent background
        else{
            wb$styles_mgr$add(
                openxlsx2::create_fill(pattern_type = "",
                                       fg_color     = NULL),
                paste0(type, "_fill"))
        }
    }

    wb
}


#' Set Up Different Font Styles
#'
#' @description
#' Pass font styles for the different parts of the table into the style manager
#' to apply them more efficiently later.
#'
#' @param wb The workbook to modify.
#' @param texts A list of the actual titles and footnotes.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a workbook with added style elements.
#'
#' @noRd
handle_font_styles <- function(wb,
                               texts = list("title" = c(), "footnote" = c()),
                               style = excel_output_style()){
    # Font can only be set globally here. Below in create_font seems to be bugged.
    wb$set_base_font(font_name = style[["font"]])

    # Add individual font styles for each table part
    for (type in c("title", "footnote", "header", "subheader", "box", "cat_col", "table")){
        if (type %in% c("title", "footnote")){
            multi_style <- max(length(style[[paste0(type, "_font_color")]]),
                               length(style[[paste0(type, "_font_size")]]),
                               length(style[[paste0(type, "_font_bold")]]))

            # For titles and footnotes multiple font colors per line can be specified
            if (multi_style > 1){
                # If there are multiple titles or footnotes, bring the formatting on
                # the same amount as the actual titles and footnotes.
                if (length(texts[[type]]) > 0){
                    multi_style <- length(texts[[type]])
                }

                style[[paste0(type, "_font_color")]] <- fill_or_trim(style[[paste0(type, "_font_color")]], multi_style)
                style[[paste0(type, "_font_size")]]  <- fill_or_trim(style[[paste0(type, "_font_size")]],  multi_style)
                style[[paste0(type, "_font_bold")]]  <- fill_or_trim(style[[paste0(type, "_font_bold")]],  multi_style)

                # Look up title font colors
                for (i in seq_len(length(style[[paste0(type, "_font_color")]]))){
                    # Normal style
                    wb$styles_mgr$add(
                        openxlsx2::create_font(sz    = style[[paste0(type, "_font_size")]][i],
                                               color = openxlsx2::wb_color(hex = style[[paste0(type, "_font_color")]][i]),
                                               b     = style[[paste0(type, "_font_bold")]][i]),
                        paste0(type, i, "_font"))

                    # Hyperlink style
                    wb$styles_mgr$add(
                        openxlsx2::create_font(sz    = style[[paste0(type, "_font_size")]][i],
                                               color = openxlsx2::wb_color(hex = "0000FF"),
                                               u     = "single"),
                        paste0(type, i, "_link_font"))
                }
            }
            else{
                # Normal style
                wb$styles_mgr$add(
                    openxlsx2::create_font(sz    = style[[paste0(type, "_font_size")]],
                                           color = openxlsx2::wb_color(hex = style[[paste0(type, "_font_color")]]),
                                           b     = style[[paste0(type, "_font_bold")]]),
                    paste0(type, "_font"))

                # Hyperlink style
                wb$styles_mgr$add(
                    openxlsx2::create_font(sz    = style[[paste0(type, "_font_size")]],
                                           color = openxlsx2::wb_color(hex = "0000FF"),
                                           u     = "single"),
                    paste0(type, "_link_font"))
            }
        }
        else{
            wb$styles_mgr$add(
                openxlsx2::create_font(sz    = style[[paste0(type, "_font_size")]],
                                       color = openxlsx2::wb_color(hex = style[[paste0(type, "_font_color")]]),
                                       b     = style[[paste0(type, "_font_bold")]]),
                paste0(type, "_font"))
        }
    }

    wb
}


#' Set Up Different Border Styles
#'
#' @description
#' Pass border styles for the different parts of the table into the style manager
#' to apply them more efficiently later.
#'
#' @param wb The workbook to modify.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a workbook with added style elements.
#'
#' @noRd
handle_border_styles <- function(wb, style = excel_output_style()){
    if (style[["header_borders"]]){
        wb$styles_mgr$add(
            openxlsx2::create_border(
                      bottom = "thin",
                      top    = "thin",
                      left   = "thin",
                      bottom_color = openxlsx2::wb_color(hex = style[["header_border_color"]]),
                      top_color    = openxlsx2::wb_color(hex = style[["header_border_color"]]),
                      left_color   = openxlsx2::wb_color(hex = style[["header_border_color"]])),
            "header_borders")
    }
    if (style[["subheader_borders"]]){
        wb$styles_mgr$add(
            openxlsx2::create_border(
                bottom = "thin",
                top    = "thin",
                bottom_color = openxlsx2::wb_color(hex = style[["subheader_border_color"]]),
                top_color    = openxlsx2::wb_color(hex = style[["subheader_border_color"]])),
            "subheader_borders")
    }
    if (style[["box_borders"]]){
        wb$styles_mgr$add(
            openxlsx2::create_border(
                      bottom = "thin",
                      top    = "thin",
                      right  = "thin",
                      bottom_color = openxlsx2::wb_color(hex = style[["box_border_color"]]),
                      top_color    = openxlsx2::wb_color(hex = style[["box_border_color"]]),
                      right_color  = openxlsx2::wb_color(hex = style[["box_border_color"]])),
            "box_borders")
    }
    if (style[["cat_col_borders"]]){
        wb$styles_mgr$add(
            openxlsx2::create_border(
                      top   = "thin",
                      right = "thin",
                      top_color   = openxlsx2::wb_color(hex = style[["cat_col_border_color"]]),
                      right_color = openxlsx2::wb_color(hex = style[["cat_col_border_color"]])),
            "cat_col_borders")
    }
    if (style[["table_borders"]]){
        wb$styles_mgr$add(
            openxlsx2::create_border(
                      top  = "thin",
                      left = "thin",
                      top_color  = openxlsx2::wb_color(hex = style[["table_border_color"]]),
                      left_color = openxlsx2::wb_color(hex = style[["table_border_color"]])),
            "table_borders")
    }

    wb$styles_mgr$add(
        openxlsx2::create_border(
            top  = "thin",
            top_color  = openxlsx2::wb_color(hex = style[["table_border_color"]])),
        "footnote_borders")

    wb
}

#' Set Up Different Number Format Styles
#'
#' @description
#' Pass number format styles for the different statistics into the style manager
#' to apply them more efficiently later.
#'
#' @param wb The workbook to modify.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a workbook with added style elements.
#'
#' @noRd
handle_number_styles <- function(wb, style = excel_output_style()){
    # Add individual font styles for each table part
    index <- 900

    for (type in c("pct", "freq", "freq.g0", "sum", "sum.wgt", "mean", "median",
                   "mode", "min", "max", "sd", "variance", "first", "last", "p",
                   "missing")){
        wb$styles_mgr$add(
            openxlsx2::create_numfmt(numFmtId   = index,
                                     formatCode = style[["number_formats"]][[paste0(type, "_excel")]]),
            paste0(type, "_numfmt"))

        index <- index + 1
    }

    wb
}


#' Set Up Styles
#'
#' @description
#' Set up all styles in the style manager to apply them more efficiently later.
#'
#' @param wb The workbook to modify.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a workbook with added style elements.
#'
#' @noRd
prepare_styles <- function(wb,
                           texts = list("title" = c(), "footnote" = c()),
                           style = excel_output_style()){
    wb |>
        handle_fill_styles(style) |>
        handle_font_styles(texts, style) |>
        handle_border_styles(style) |>
        handle_number_styles(style)
}

###############################################################################
# Handle cell dimensions
###############################################################################
#' Handle Column Widths and Row Heights
#'
#' @description
#' Handle column width and row heights depending on specified options.
#'
#' @param wb The workbook to modify.
#' @param ranges Table ranges to format.
#' @param number_of_columns The number of columns that should be formatted.
#' @param number_of_rows The number of rows that should be formatted.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a further formatted workbook.
#'
#' @noRd
handle_col_row_dimensions <- function(wb,
                                      ranges,
                                      number_of_columns,
                                      number_of_rows,
                                      style = excel_output_style()){
    column_widths <- style[["column_widths"]]
    start_column  <- 1
    end_column    <- style[["start_column"]] + (number_of_columns - 1)

    # If specific column widths are specified
    if (length(column_widths) > 1){
        column_widths <- fill_or_trim(column_widths,
                                      end_column)
    }
    # If only one column width is specified
    else if (column_widths != "auto"){
        column_widths <- fill_or_trim(column_widths,
                                      end_column)
    }
    # On auto format only format row header columns. On large workbooks this
    # is very slow so only make sure the text in front are readable.
    else{
        start_column      <- ranges[["header.column"]]
        number_of_columns <- ranges[["header.column"]] + (ranges[["cat_col.width"]] - 1)
    }

    row_heights <- style[["row_heights"]]
    start_row   <- 1
    end_row     <- style[["start_row"]] + (number_of_rows - 1)

    # If specific column widths are specified
    if (length(row_heights) > 1){
        row_heights <- fill_or_trim(row_heights,
                                    end_row)
    }
    # If only one row height is specified
    else if (row_heights != "auto"){
        row_heights <- fill_or_trim(row_heights,
                                    end_row)
    }
    else{
        row_heights <- NULL
    }

    wb$set_col_widths(cols   = start_column:end_column,
                      widths = column_widths)
    wb$set_row_heights(rows    = start_row:end_row,
                       heights = row_heights)

    wb
}


#' Handle Automatic Dimensions
#'
#' @description
#' Handle column width and row heights if they should be automatically adjusted.
#'
#' @param wb The workbook to modify.
#' @param ranges Table ranges to format.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a further formatted workbook.
#'
#' @noRd
handle_auto_dimensions <- function(wb,
                                   ranges,
                                   style = excel_output_style()){
    # If autofit columns is selected, set a manual size after the first table
    # column which is sufficient in most cases. The integrated autofit often
    # gives to narrow columns.
    column_width <- style[["column_widths"]]

    if (length(column_width) == 1){
        if (column_width == "auto"){
            start_col <- style[["start_column"]] + 1

            wb$set_col_widths(cols   = start_col:ranges[["table.end"]],
                              widths = 10)
        }
    }

    # If autofit heights is selected, set a manual size for the table header,
    # since this is the only row to safely say that it has multi row text.
    # There is no good solution to handle the other row. One could only make
    # a vague guess because the actual space each individual font takes at different
    # sizes is unclear. For now the user has to handle multi row texts manually.
    row_heights <- style[["row_heights"]]

    if (length(row_heights) == 1){
        if (row_heights == "auto"){
            wb$set_row_heights(rows    = ranges[["header.row"]],
                               heights = 30)
        }
    }

    wb
}


#' Handle Automatic Dimensions For Any_Table
#'
#' @description
#' Handle column width and row heights if they should be automatically adjusted in
#' an any_table.
#'
#' @param wb The workbook to modify.
#' @param ranges Table ranges to format.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a further formatted workbook.
#'
#' @noRd
handle_any_auto_dimensions <- function(wb,
                                       ranges,
                                       style = excel_output_style()){
    # If autofit columns is selected, set a manual size after the table row header
    # columns which is sufficient in most cases. The integrated autofit often
    # gives to narrow columns.
    column_width <- style[["column_widths"]]

    if (length(column_width) == 1){
        if (column_width == "auto"){
            start_col <- style[["start_column"]] + ranges[["cat_col.width"]]

            wb$set_col_widths(cols   = start_col:ranges[["table.end"]],
                              widths = 9)
        }
    }

    wb
}


#' Handle Header and Table Heights
#'
#' @description
#' Handle header and table row heights if they should be adjusted individually.
#'
#' @param wb The workbook to modify.
#' @param ranges Table ranges to format.
#' @param style A list of style elements to format the table.
#'
#' @return
#' Returns a further formatted workbook.
#'
#' @noRd
handle_header_table_dim <- function(wb,
                                    ranges,
                                    style = excel_output_style()){
    # Set row heights for header if specified
    header_heights <- style[["header_heights"]]

    if (!is.null(header_heights)){
        number_of_rows <- max(ranges[["header.length"]], 1)
        start_row      <- ranges[["header.row"]]
        end_row        <- start_row + max((number_of_rows - 1), 0)

        header_heights <- fill_or_trim(header_heights,
                                       number_of_rows)

        wb$set_row_heights(rows    = start_row:end_row,
                           heights = header_heights)
    }

    # Set row heights for table if specified
    table_heights <- style[["table_heights"]]

    if (!is.null(table_heights)){
        number_of_rows <- ranges[["table.length"]]
        start_row      <- ranges[["table.row"]]
        end_row        <- start_row + (number_of_rows - 1)

        table_heights <- fill_or_trim(table_heights,
                                      number_of_rows)

        wb$set_row_heights(rows    = start_row:end_row,
                           heights = table_heights)
    }

    wb
}


#' Adjust Format Vector to Needed Length
#'
#' @description
#' Either fill up or trim down the provided format vector to the needed number
#' of columns/rows which have to be formatted.
#'
#' @param format_vector Format vector to be adjusted.
#' @param number_to_format Needed length to be adjusted.
#'
#' @return
#' Returns an adjusted format vector.
#'
#' @noRd
fill_or_trim <- function(format_vector,
                         number_to_format){
    # If length of provided vector is lesser than there are places to format
    if (length(format_vector) < number_to_format){
        # Repeat the last vector element until the number to format is reached
        format_vector <- c(format_vector,
                           rep(utils::tail(format_vector, 1),
                               number_to_format - length(format_vector)))
    }
    # If length of provided vector is equal or greater than there are places to format
    else{
        # Cut elements down to number of places to format
        format_vector <- utils::head(format_vector, number_to_format)
    }

    format_vector
}


#' Split Up 'Excel' Ranges
#'
#' @description
#' Split up a range into individual column ranges.
#'
#' @param range Range to be split up.
#'
#' @return
#' Returns vector of column ranges.
#'
#' @noRd
split_up_ranges <- function(range){
    # Extract the row numbers from range
    rows <- unlist(regmatches(range, gregexpr("[0-9]+", range)))

    start_row <- rows[1]
    end_row   <- rows[2]

    # Extract the column letters from range
    columns <- unlist(regmatches(range, gregexpr("[A-Z]+", range)))

    # Build a sequence of letters for the provided range
    column_numbers  <- openxlsx2::col2int(paste(columns[1], columns[2], sep = ":"))
    column_sequence <- openxlsx2::int2col(column_numbers)

    # Combine everything back together to individual columns
    paste0(column_sequence, start_row, ":", column_sequence, end_row)
}

###############################################################################
# Style options
###############################################################################
#' Style for 'Excel' Table Outputs
#'
#' @description
#' Set different options which define the visual output of 'Excel' tables produced
#' by [frequencies()], [crosstabs()] and [any_table()].
#'
#' @param save_path If NULL, opens the output as temporary file. Otherwise specify an output path.
#' @param file If NULL, opens the output as temporary file. Otherwise specify a filename with extension.
#' @param sheet_name Name of the sheet inside the workbook to which the output shall be written.
#' If multiple outputs are produced in one go, the sheet name additionally receives a running number.
#' @param font Set the font to be used for the entire output.
#' @param column_widths Specify whether column widths should be set automatically and individually or
#' if a numeric vector is passed each column width can be specified manually. If a table has more
#' columns than column widths are provided, the last given column width will be repeated until the
#' end of the table.
#' @param row_heights Specify whether row heights should be set automatically and individually or
#' if a numeric vector is passed each row height can be specified manually. If a table has more
#' rows than row heights are provided, the last given row height will be repeated until the
#' end of the table.
#' @param title_heights Set individual row heights for the titles only.
#' @param header_heights Set individual row heights for the table header only.
#' @param subheader_heights Set individual row heights for the table subheader only.
#' @param table_heights Set individual row heights for the table body only.
#' @param footnote_heights Set individual row heights for the footnotes only.
#' @param start_row The row in which the table starts.
#' @param start_column The column in which the table starts.
#' @param freeze_col_header Whether to freeze the column header so that it is always visible
#' while scrolling down the document.
#' @param freeze_row_header Whether to freeze the row header so that it is always visible
#' while scrolling sideways in the document.
#' @param filters Whether to set filters in the column header, when exporting a data frame.
#' @param grid_lines Whether to show grid lines or not.
#' @param by_as_subheaders Whether to format by variables as subheaders in one table instead
#' of single tables on multiple sheets.
#' @param background_color Background cell color for any cell which isn't covered by the other
#' background color options.
#' @param header_back_color Background cell color of the table header.
#' @param header_font_color Font color of the table header.
#' @param header_font_size Font size of the table header.
#' @param header_font_bold Whether to print the table header in bold letters.
#' @param header_alignment Set the text alignment of the table header.
#' @param header_stat_merging Set how far the statistics row is merged. Allowed are "all",
#' "block" (default), "none".
#' @param header_wrap Whether to wrap the texts in the table header.
#' @param header_indent Indentation level of the table header.
#' @param header_borders Whether to draw borders around the table header cells.
#' @param header_border_color Borders colors of the table header cells.
#' @param subheader_back_color Background cell color of the table subheader.
#' @param subheader_font_color Font color of the table subheader.
#' @param subheader_font_size Font size of the table subheader.
#' @param subheader_font_bold Whether to print the table subheader in bold letters.
#' @param subheader_alignment Set the text alignment of the table subheader.
#' @param subheader_wrap Whether to wrap the texts in the table subheader.
#' @param subheader_indent Indentation level of the table subheader.
#' @param subheader_borders Whether to draw borders around the table subheader cells.
#' @param subheader_border_color Borders colors of the table subheader cells.
#' @param cat_col_back_color Background cell color of the category columns inside the table.
#' @param cat_col_font_color Font color of the category columns inside the table.
#' @param cat_col_font_size Font size of the category columns inside the table.
#' @param cat_col_font_bold Whether to print the category columns inside the table in bold letters.
#' @param cat_col_alignment Set the text alignment of the category columns inside the table.
#' @param cat_col_wrap Whether to wrap the texts in the category columns inside the table.
#' @param cat_col_indent Indentation level of the category columns inside the table.
#' @param cat_col_borders Whether to draw borders around the category columns inside the table.
#' @param cat_col_border_color Borders colors of the category columns inside the table.
#' @param table_back_color Background color of the inner table cells.
#' @param table_font_color Font color of the inner table cells.
#' @param table_font_size Font size of the inner table cells.
#' @param table_font_bold Whether to print the inner table cells in bold numbers
#' @param table_alignment Set the text alignment of the inner table cells.
#' @param table_indent Indentation level of the inner table cells.
#' @param table_borders Whether to draw borders around the inner table cells.
#' @param table_border_color Borders colors of the inner table cells.
#' @param as_heatmap Whether to lay a conditional formatting over the values.
#' @param heatmap_low_color The color for lower values in the conditional formatting.
#' @param heatmap_middle_color The color for middle values in the conditional formatting.
#' @param heatmap_high_color The color for high values in the conditional formatting.
#' @param box_back_color Background color of the left box in table header.
#' @param box_font_color Font color of the left box in table header.
#' @param box_font_size Font size of the left box in table header.
#' @param box_font_bold Whether to print the left box in table header in bold letters.
#' @param box_alignment Set the text alignment of the left box in table header.
#' @param box_wrap Whether to wrap the texts in the left box in table header.
#' @param box_indent Indentation level of the left box in table header.
#' @param box_borders Whether to draw borders around the left box in table header.
#' @param box_border_color Borders colors of the left box in table header.
#' @param number_formats Put in a list of number formats which should be assigned to
#' the different stats. Number formats can be created with [number_format_style()].
#' @param title_font_color Font color of the titles.
#' @param title_font_size Font size of the tables titles.
#' @param title_font_bold Whether to print the tables titles in bold letters.
#' @param title_alignment Set the text alignment of the titles.
#' @param footnote_font_color Font color of the footnotes
#' @param footnote_font_size Font size of the tables footnotes
#' @param footnote_font_bold Whether to print the tables footnotes in bold letters.
#' @param footnote_alignment Set the text alignment of the footnotes.
#' @param na_symbol Define the symbol that should be used for NA values.
#'
#' @details
#' [excel_output_style()] is based on the Output Delivery System (ODS) in 'SAS',
#' which provides efficient and readable ways to set up different table styles.
#'
#' With the output style you have full control over the table design. There is no
#' need to think about calculating the right place to input a background color or a
#' border of a certain type and how to do this in a loop for multiple cells. Just
#' input colors, borders, font styles, etc. for the different table parts and everything
#' else is handled by the functions capable of using styles.
#'
#' The concept basically is: design over complex calculations.
#'
#' @return
#' Returns a list of named style options.
#'
#' @seealso
#' Creating a custom table style: [modify_output_style()],
#' [number_format_style()], [modify_number_formats()].
#'
#' Global style options: [set_style_options()], [set_labels()].
#'
#' Functions that can handle styles: [frequencies()], [crosstabs()], [any_table()],
#' [export_with_style()]
#'
#' @examples
#' # For default values
#' excel_style <- excel_output_style()
#'
#' # Set specific options, the rest will be set to default values
#' excel_style <- excel_output_style(font       = "Calibri",
#'                                   sheet_name = "My_Output")
#'
#' # For cells with no background color pass an empty string
#' excel_style <- excel_output_style(table_back_color = "")
#'
#' @export
excel_output_style <- function(save_path              = NULL,
                               file                   = NULL,
                               sheet_name             = "Table",
                               font                   = "Arial",
                               column_widths          = "auto",
                               row_heights            = "auto",
                               title_heights          = NULL,
                               header_heights         = NULL,
                               subheader_heights      = NULL,
                               table_heights          = NULL,
                               footnote_heights       = NULL,
                               start_row              = 2,
                               start_column           = 2,
                               freeze_col_header      = FALSE,
                               freeze_row_header      = FALSE,
                               filters                = TRUE,
                               grid_lines             = TRUE,
                               by_as_subheaders       = FALSE,
                               background_color       = "FFFFFF",
                               header_back_color      = "FFFFFF",
                               header_font_color      = "000000",
                               header_font_size       = 10,
                               header_font_bold       = TRUE,
                               header_alignment       = "center",
                               header_stat_merging    = "block",
                               header_wrap            = "1",
                               header_indent          = 0,
                               header_borders         = TRUE,
                               header_border_color    = "000000",
                               subheader_back_color   = "FFFFFF",
                               subheader_font_color   = "000000",
                               subheader_font_size    = 10,
                               subheader_font_bold    = TRUE,
                               subheader_alignment    = "center",
                               subheader_wrap         = "1",
                               subheader_indent       = 0,
                               subheader_borders      = TRUE,
                               subheader_border_color = "000000",
                               cat_col_back_color     = "FFFFFF",
                               cat_col_font_color     = "000000",
                               cat_col_font_size      = 10,
                               cat_col_font_bold      = FALSE,
                               cat_col_alignment      = "left",
                               cat_col_wrap           = "1",
                               cat_col_indent         = 1,
                               cat_col_borders        = TRUE,
                               cat_col_border_color   = "000000",
                               table_back_color       = "FFFFFF",
                               table_font_color       = "000000",
                               table_font_size        = 10,
                               table_font_bold        = FALSE,
                               table_alignment        = "right",
                               table_indent           = 1,
                               table_borders          = FALSE,
                               table_border_color     = "000000",
                               as_heatmap             = FALSE,
                               heatmap_low_color      = "F8696B",
                               heatmap_middle_color   = "FFFFFF",
                               heatmap_high_color     = "63BE7B",
                               box_back_color         = "FFFFFF",
                               box_font_color         = "000000",
                               box_font_size          = 10,
                               box_font_bold          = TRUE,
                               box_alignment          = "center",
                               box_wrap               = "1",
                               box_indent             = 0,
                               box_borders            = TRUE,
                               box_border_color       = "000000",
                               number_formats         = number_format_style(),
                               title_font_color       = "000000",
                               title_font_size        = 10,
                               title_font_bold        = TRUE,
                               title_alignment        = "left",
                               footnote_font_color    = "000000",
                               footnote_font_size     = 8,
                               footnote_font_bold     = FALSE,
                               footnote_alignment     = "left",
                               na_symbol              = "."){

    as.list(environment())
}


#' Modify Style for 'Excel' Table Outputs
#'
#' @description
#' Modify a previously created style with [excel_output_style()].
#'
#' @param style_to_modify A pre created style where only certain elements should be
#' modified while the rest is kept as is.
#' @param ... Pass in names and corresponding new values for existing style elements.
#'
#' @details
#' [modify_output_style()] is based on the Output Delivery System (ODS) in 'SAS',
#' which provides efficient and readable ways to set up different table styles.
#'
#' With the output style you have full control over the table design. There is no
#' need to think about calculating the right place to input a background color or a
#' border of a certain type and how to do this in a loop for multiple cells. Just
#' input colors, borders, font styles, etc. for the different table parts and everything
#' else is handled by the functions capable of using styles.
#'
#' The concept basically is: design over complex calculations.
#'
#' @return
#' Returns a modified list of named style options.
#'
#' @seealso
#' Creating a custom table style: [excel_output_style()],
#' [number_format_style()], [modify_number_formats()].
#'
#' Global style options: [set_style_options()], [set_labels()].
#'
#' Functions that can handle styles: [frequencies()], [crosstabs()], [any_table()],
#' [export_with_style()]
#'
#' @examples
#' # For default values
#' excel_style <- excel_output_style()
#'
#' # Set specific options, the rest will be kept as is
#' excel_style <- excel_style |> modify_output_style(sheet_name      = "Sheet",
#'                                                   title_font_bold = FALSE)
#'
#' # For cells with no background color pass an empty string
#' excel_style <- excel_style |> modify_output_style(table_back_color = "")
#'
#' @export
modify_output_style <- function(style_to_modify, ...){
    style_elements <- list(...)

    # Loop through all elements to modify and if exists then set new value
    for (element in seq_along(style_elements)){
        name <- names(style_elements)[element]

        if (!name %in% names(style_to_modify)){
            print_message("WARNING", "Style element '{name}' is invalid and will be omitted.", name = name)
        }

        style_to_modify[[name]] <- style_elements[[element]]
    }

    style_to_modify
}


###############################################################################
# Number formats for any_table
###############################################################################
#' Number Formats Used by [any_table()]
#'
#' @description
#' Set individual number formats for the different statistics in tables produced
#' with [any_table()].
#'
#' @param pct_excel Number format for percentage applied in Excel workbook.
#' @param freq_excel Number format for frequency applied in Excel workbook.
#' @param freq.g0_excel Number format for frequency greater zero applied in Excel workbook.
#' @param sum_excel Number format for sum applied in Excel workbook.
#' @param sum.wgt_excel Number format for sum of weights applied in Excel workbook.
#' @param mean_excel Number format for mean applied in Excel workbook.
#' @param median_excel Number format for median applied in Excel workbook.
#' @param mode_excel Number format for mode applied in Excel workbook.
#' @param min_excel Number format for min applied in Excel workbook.
#' @param max_excel Number format for max applied in Excel workbook.
#' @param sd_excel Number format for sd applied in Excel workbook.
#' @param variance_excel Number format for variance applied in Excel workbook.
#' @param first_excel Number format for first applied in Excel workbook.
#' @param last_excel Number format for last applied in Excel workbook.
#' @param missing_excel Number format for missing applied in Excel workbook.
#' @param p_excel Number format for percentile applied in Excel workbook.
#' @param pct_decimals Number of decimals for percentage.
#' @param freq_decimals Number of decimals for frequency.
#' @param freq.g0_decimals Number of decimals for frequency greater zero.
#' @param sum_decimals Number of decimals for sum.
#' @param sum.wgt_decimals Number of decimals for sum of weights.
#' @param mean_decimals Number of decimals for mean.
#' @param median_decimals Number of decimals for median.
#' @param mode_decimals Number of decimals for mode.
#' @param min_decimals Number of decimals for min.
#' @param max_decimals Number of decimals for max.
#' @param sd_decimals Number of decimals for sd.
#' @param variance_decimals Number of decimals for variance.
#' @param first_decimals Number of decimals for first.
#' @param last_decimals Number of decimals for last.
#' @param missing_decimals Number of decimals for missing.
#' @param p_decimals Number of decimals for percentile.
#'
#' @details
#' [number_format_style()] is based on 'SAS' number formats and the Output Delivery
#' System (ODS), which provides efficient and readable ways to set up different table
#' styles.
#'
#' With the number format style you have full control over formatting numbers according
#' to the different statistics. There is no need to think about calculating the right
#' place to input the number formats and how to do this in a loop for multiple cells.
#' Just input the different number formats and decimals for the different statistics
#' and everything else is handled by the functions capable of using number styles.
#'
#' The concept basically is: design over complex calculations.
#'
#' @return
#' Returns a list of named number format options.
#'
#' @seealso
#' Creating a custom table style: [excel_output_style()], [modify_output_style()],
#' [modify_number_formats()].
#'
#' Global style options: [set_style_options()], [set_labels()].
#'
#' Functions that can handle styles: [frequencies()], [crosstabs()], [any_table()],
#' [export_with_style()]
#'
#' @examples
#' # For default values
#' format_list <- number_format_style()
#'
#' # Set specific options, the rest will be set to default values
#' format_list <- number_format_style(pct_excel    = "0.00000000",
#'                                    pct_decimals = 8)
#'
#' # IMPORTANT: Don't forget to add individual formats to an excel style, otherwise
#' # they won't come into affect.
#' excel_style <- excel_output_style(number_formats = format_list)
#'
#' @export
number_format_style <- function(pct_excel         = "0.0",
                                freq_excel        = "#,###,##0",
                                freq.g0_excel     = "#,###,##0",
                                sum_excel         = "#,###,##0",
                                sum.wgt_excel     = "#,###,##0",
                                mean_excel        = "#,###,##0",
                                median_excel      = "#,###,##0",
                                mode_excel        = "#,###,##0",
                                min_excel         = "#,###,##0",
                                max_excel         = "#,###,##0",
                                sd_excel          = "#,###,##0.000",
                                variance_excel    = "#,###,##0.000",
                                first_excel       = "#,###,##0",
                                last_excel        = "#,###,##0",
                                p_excel           = "#,###,##0",
                                missing_excel     = "#,###,##0",
                                pct_decimals      = 1,
                                freq_decimals     = 0,
                                freq.g0_decimals  = 0,
                                sum_decimals      = 3,
                                sum.wgt_decimals  = 3,
                                mean_decimals     = 2,
                                median_decimals   = 2,
                                mode_decimals     = 2,
                                min_decimals      = 2,
                                max_decimals      = 2,
                                sd_decimals       = 3,
                                variance_decimals = 3,
                                first_decimals    = 0,
                                last_decimals     = 0,
                                p_decimals        = 2,
                                missing_decimals  = 0){

    as.list(environment())
}


#' Modify Number Formats Used by [any_table()]
#'
#' @description
#' Modify previously created number formats with [number_format_style()].
#'
#' @param formats_to_modify Pre created number formats where only certain elements
#' should be modified while the rest is kept as is.
#' @param ... Pass in names and corresponding new values for existing number formats.
#'
#' @details
#' [modify_number_formats()] is based on 'SAS' number formats and the Output Delivery
#' System (ODS), which provides efficient and readable ways to set up different table
#' styles.
#'
#' With the number format style you have full control over formatting numbers according
#' to the different statistics. There is no need to think about calculating the right
#' place to input the number formats and how to do this in a loop for multiple cells.
#' Just input the different number formats and decimals for the different statistics
#' and everything else is handled by the functions capable of using number styles.
#'
#' The concept basically is: design over complex calculations.
#'
#' @return
#' Returns a modified list of number format options.
#'
#' @seealso
#' Creating a custom table style: [excel_output_style()], [modify_output_style()],
#' [number_format_style()].
#'
#' Global style options: [set_style_options()], [set_labels()].
#'
#' Functions that can handle styles: [frequencies()], [crosstabs()], [any_table()],
#' [export_with_style()].
#'
#' @examples
#' # For default values
#' format_list <- number_format_style(pct_excel    = "0.00000000",
#'                                    pct_decimals = 8)
#'
#' # Set specific options, the rest will be kept as is
#' format_list <- format_list |> modify_number_formats(sum_excel = "#,###,##0.000")
#'
#' # IMPORTANT: Don't forget to add individual formats to an excel style, otherwise
#' # they won't come into affect.
#' excel_style <- excel_output_style(number_formats = format_list)
#'
#' @export
modify_number_formats <- function(formats_to_modify, ...){
    format_elements <- list(...)

    # Loop through all elements to modify and if exists then set new value
    for (element in seq_along(format_elements)){
        name <- names(format_elements)[element]

        if (!name %in% names(formats_to_modify)){
            print_message("WARNING", "Number format '[name]' is invalid and will be omitted.", name = name)
        }

        formats_to_modify[[name]] <- format_elements[[element]]
    }

    formats_to_modify
}
