#' Generate benchmarking graphics in a PDF file
#'
#'
#' @description
#'
#' \if{html,text}{(\emph{version française: 
#' \url{https://StatCan.github.io/gensol-gseries/fr/reference/plot_graphTable.html}})}
#' 
#' Create a PDF file (US Letter paper size format in landscape view) containing benchmarking graphics for the set of series 
#' contained in the specified benchmarking function ([benchmarking()] or [stock_benchmarking()]) output `graphTable` 
#' data frame. Four types of benchmarking graphics can be generated for each series:
#' - **Original Scale Plot** (argument `ori_plot_flag`) - overlay graph of:
#'   - Indicator series
#'   - Average indicator series
#'   - Bias corrected indicator series (when \eqn{\rho < 1})
#'   - Benchmarked series
#'   - Average benchmark
#' - **Adjustment Scale Plot** (argument `adj_plot_flag`) - overlay graph of:
#'   - Benchmarking adjustments
#'   - Average benchmarking adjustments
#'   - Bias line (when \eqn{\rho < 1})
#' - **Growth Rates Plot** (argument `GR_plot_flag`) - bar chart of the indicator and benchmarked series growth rates.
#' - **Growth Rates Table** (argument `GR_table_flag`) - table of the indicator and benchmarked series growth rates.
#'
#' These graphics can be useful to assess the quality of the benchmarking results. Any of the four types of
#' benchmarking graphics can be enabled or disabled with the corresponding flag. The first three types of graphics 
#' (the plots) are generated by default while the fourth (growth rates table) is not.
#'
#'
#' @param graphTable (mandatory)
#'
#' Data frame (object of class "data.frame") corresponding to the benchmarking function output`graphTable` data frame.
#'
#' @param pdf_file (mandatory)
#'
#' Name (and path) of the PDF file that will contain the benchmarking graphics. The name should include the ".pdf" 
#' file extension. The PDF file is created in the R session working directory (as returned by `getwd()`) if a path 
#' is not specified. Specifying `NULL` would cancel the creation of a PDF file.
#'
#' @param ori_plot_flag,adj_plot_flag,GR_plot_flag,GR_table_flag (optional)
#'
#' Logical arguments indicating whether or not the corresponding type of benchmarking graphic should be generated. 
#' All three plots are generated by default but not the growth rates tables.
#'
#' **Default values** are `ori_plot_flag = TRUE`, `adj_plot_flag = TRUE`, `GR_plot_flag = TRUE` and 
#' `GR_table_flag = FALSE`.
#'
#' @param add_bookmarks
#'
#' Logical argument indicating whether or not bookmarks should be added to the PDF file. See **Bookmarks** in section 
#' **Details** for more information.
#'
#' **Default value** is `add_bookmarks = TRUE`.
#'
#'
#' @details
#' List of the `graphTable` data frame variables corresponding to each element of the four types of benchmarking 
#' graphics:
#' - Original Scale Plot (argument `ori_plot_flag`)
#'   - `subAnnual` for the *Indicator Series* line
#'   - `avgSubAnnual` for the *Avg. Indicator Series* segments
#'   - `subAnnualCorrected` for the *Bias Corr. Indicator Series* line (when \eqn{\rho < 1})
#'   - `benchmarked` for the *Benchmarked Series* line
#'   - `avgBenchmark` for the *Average Benchmark* segments
#' - Adjustment Scale Plot (argument `adj_plot_flag`)
#'   - `benchmarkedSubAnnualRatio` for the *BI Ratios (Benchmarked Series / Indicator Series)* line \eqn{^{(*)}}{(*)}
#'   - `avgBenchmarkSubAnnualRatio` for the *Average BI Ratios* segments \eqn{^{(*)}}{(*)}
#'   - `bias` for the *Bias* line (when \eqn{\rho < 1})
#' - Growth Rates Plot (argument `GR_plot_flag`)
#'   - `growthRateSubAnnual` for the *Growth R. in Indicator Series* bars \eqn{^{(*)}}{(*)}
#'   - `growthRateBenchmarked` for the *Growth R. in Benchmarked Series* bars \eqn{^{(*)}}{(*)}
#' - Growth Rates Table (argument `GR_table_flag`)
#'   - `year` for the *Year* column
#'   - `period` for the *Period* column
#'   - `subAnnual` for the *Indicator Series* column
#'   - `benchmarked` for the *Benchmarked Series* column
#'   - `growthRateSubAnnual` for the *Growth Rate in Indicator Series* column \eqn{^{(*)}}{(*)}
#'   - `growthRateBenchmarked` for the *Growth Rate in Benchmarked Series* column \eqn{^{(*)}}{(*)}
#' 
#' \eqn{^{(*)}}{(*)} _BI ratios_ and _growth rates_ actually correspond to _differences_ when \eqn{\lambda = 0} (additive
#' benchmarking).
#' 
#' The function uses the extra columns of the `graphTable` data frame (columns not listed in the **Value** section of 
#' [benchmarking()] and [stock_benchmarking()]), if any, to build BY-groups. See section **Benchmarking Multiple Series**  
#' of [benchmarking()] for more details.
#' 
#' ## Performance
#' The two types of growth rates graphics, i.e., the bar chart (`GR_plot_flag`) and table (`GR_table_flag`), often requires 
#' the generation of several pages in the PDF file, especially for long monthly series with several years of data. This creation of 
#' extra pages slows down the execution of [plot_graphTable()]. This is why only the bar chart is generated by default 
#' (`GR_plot_flag = TRUE` and `GR_table_flag = FALSE`). Deactivating both types of growth rates graphics (`GR_plot_flag = FALSE` 
#' and `GR_table_flag = FALSE`) or reducing the size of the input `graphTable` data frame for very long series (e.g., keeping 
#' only recent years) could therefore improve execution time. Also note that the impact of benchmarking on the growth rates can be 
#' deduced from the adjustment scale plot (`adj_plot_flag`) by examining the extent of vertical movement (downward or upward) of the 
#' benchmarking adjustments between adjacent periods: the greater the vertical movement, the greater the impact on corresponding growth 
#' rate. Execution time of [plot_graphTable()] could therefore be reduced, if needed, by only generating the first two types of graphics 
#' while focusing on the adjustment scale plot to assess period-to-period movement preservation, i.e., the impact of benchmarking on 
#' the initial growth rates.
#' 
#' ## ggplot2 themes
#' The plots are generated with the ggplot2 package which comes with a convenient set of [complete 
#' themes](https://ggplot2.tidyverse.org/reference/ggtheme.html) for the general look and feel of the plots (with `theme_grey()` 
#' as the default theme). Use function `theme_set()` to change the theme applied to the plots generated by [plot_graphTable()] 
#' (see the **Examples**).
#' 
#' ## Bookmarks
#' Bookmarks are added to the PDF file with `xmpdf::set_bookmarks()` when argument `add_bookmarks = TRUE` (default), which 
#' requires a command-line tool such as [Ghostscript](https://www.ghostscript.com/)  or 
#' [PDFtk](https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/). See section **Installation** in 
#' `vignette("xmpdf", package = "xmpdf")` for details.
#' 
#' **Important**: bookmarks will be successfully added to the PDF file **if and only if** \ifelse{latex}{\code{xmpdf::supports 
#' _set_bookmarks()}}{\code{xmpdf::supports_set_bookmarks()}} returns `TRUE` **and** the execution of `xmpdf::set_bookmarks()` is 
#' successful. If Ghostscript is installed on your machine but `xmpdf::supports_set_bookmarks()` still returns `FALSE`, try 
#' specifying the path of the Ghostscript executable in environment variable `R_GSCMD` (e.g., 
#' `Sys.setenv(R_GSCMD = "C:/Program Files/.../bin/gswin64c.exe")` on Windows). On the other hand, if 
#' `xmpdf::supports_set_bookmarks()}` returns `TRUE` but you are experiencing (irresolvable) issues with `xmpdf::set_bookmarks()` 
#' (e.g., error related to the Ghostscript executable), bookmarks creation can be disabled by specifying `add_bookmarks = FALSE`.
#'
#'
#' @returns
#' In addition to creating a PDF file containing the benchmarking graphics (except when `pdf_file = NULL`), this function 
#' also invisibly returns a list with the following elements: 
#' - `graphTable`: Character string (character vector of length one) that contains the complete name and path of the PDF 
#' file if it was successfully created and `invisible(NA_character_)` otherwise or if `pdf_file = NULL` was specified.
#' - `graph_list`: List of the generated benchmarking graphics (one per series) with the following elements:
#'   - `name`: Character string describing the series (matches the bookmark name in the PDF file).
#'   - `page`: Integer representing the sequence number of the first graphic for the series in the entire sequence of 
#'   graphics for all series (matches the page number in the PDF file).
#'   - `ggplot_list`: List of ggplot objects (one per graphic or page in the PDF file) corresponding to the generated 
#'   benchmarking graphics for the series. See section **Value** in [bench_graphs] for details.
#' 
#' Note that the returned ggplot objects can be displayed _manually_ with [print()], in which case some updates to the 
#' ggplot2 theme defaults are recommended in order to produce graphics with a similar look and feel as those generated in 
#' the PDF file (see section **Value** in [bench_graphs] for details). Also keep in mind that these graphics are optimized 
#' for the US Letter paper size format in landscape view (as displayed in the PDF file), i.e., 11in wide (27.9cm, 1056px 
#' with 96 DPI) and 8.5in tall (21.6cm, 816px with 96 DPI).
#'
#'
#' @seealso [bench_graphs] [plot_benchAdj()] [benchmarking()] [stock_benchmarking()]
#'
#'
#' @example misc/function_examples/plot_graphTable-ex.R
#' 
#' 
#' @export
plot_graphTable <- function(graphTable,
                            pdf_file,
                            ori_plot_flag = TRUE,
                            adj_plot_flag = TRUE,
                            GR_plot_flag = TRUE,
                            GR_table_flag = FALSE,
                            add_bookmarks = TRUE) {
  
  
  # Initialize the object to be returned by the function via `on.exit()`
  out_list <- list(pdf_name = NA_character_,
                   graph_list = NULL)
  on.exit(return(invisible(out_list)))
  
  # Turn off debugging/traceback generated by calls to the stop() function inside internal functions
  ini_error_opt <- getOption("error")
  on.exit(options(error = ini_error_opt), add = TRUE)
  options(error = NULL)
  
  # Display warnings immediately (equivalent of `warning(..., immediate. = TRUE)` for warning messages
  # not written/generated by you directly)
  ini_warn_opt <- getOption("warn")
  on.exit(options(warn = ini_warn_opt), add = TRUE)
  options(warn = 1)
  
  
  # Validation
  df_name <- deparse1(substitute(graphTable))
  tmp <- nchar(df_name)
  if (tmp == 0) {
    stop("Argument 'graphTable' is mandatory (it must be specified).\n\n", call. = FALSE)
  } else if (tmp >= 60) {
    df_name <- paste0(substr(df_name, 1, 55), "<...>")
  }
  if (grepl("structure(", df_name, fixed = TRUE)) {
    df_name <- "<argument 'graphTable'>"
  }
  graphTable <- graphTable
  if (!is.data.frame(graphTable)) {
    stop("Argument 'graphTable' is not a 'data.frame' object.\n\n", call. = FALSE)
  }
  graphTable <- as.data.frame(graphTable)
  ori_plot_flag <- gs.validate_arg_logi(ori_plot_flag)
  adj_plot_flag <- gs.validate_arg_logi(adj_plot_flag)
  GR_plot_flag <- gs.validate_arg_logi(GR_plot_flag)
  GR_table_flag <- gs.validate_arg_logi(GR_table_flag)
  add_bookmarks <- gs.validate_arg_logi(add_bookmarks)
  
  
  # Set mandatory argument `pdf_file` to NULL if it's not specified
  # (do not create a PDF file)
  if (nchar(deparse1(substitute(pdf_file))) == 0) {
    pdf_file <- NULL
  }
  
  
  
  # Validate the graph table contents
  df_cols <- names(graphTable)
  mandatory_cols <- c("varSeries",
                      "m",
                      "year",
                      "period",
                      "constant",
                      "rho",
                      "lambda",
                      "bias",
                      "periodicity",
                      "subAnnual",
                      "benchmarked",
                      "avgBenchmark",
                      "avgSubAnnual",
                      "subAnnualCorrected",
                      "benchmarkedSubAnnualRatio",
                      "avgBenchmarkSubAnnualRatio",
                      "growthRateSubAnnual",
                      "growthRateBenchmarked")
  missing_cols <- setdiff(mandatory_cols, df_cols)
  if (length(missing_cols) > 0) {
    stop("The following columns are missing from the 'graphTable' data frame (", df_name, "): ",
         paste0("\n  ", missing_cols, collapse = ""), "\n\n", call. = FALSE)
  }
  
  # Get the by-group columns ("extras" columns)
  by_cols <- setdiff(df_cols, 
                     c("varSeries",
                       "varBenchmarks",
                       "altSeries",
                       "altSeriesValue",
                       "altbenchmarks",
                       "altBenchmarksValue",
                       "t",
                       "m",
                       "year",
                       "period",
                       "constant",
                       "rho",
                       "lambda",
                       "bias",
                       "periodicity",
                       "date",
                       "subAnnual",
                       "benchmarked",
                       "avgBenchmark",
                       "avgSubAnnual",
                       "subAnnualCorrected",
                       "benchmarkedSubAnnualRatio",
                       "avgBenchmarkSubAnnualRatio",
                       "growthRateSubAnnual",
                       "growthRateBenchmarked"))
  
  # Determine the set of series to plot (combination of by-group column values plus column "varSeries" values)
  ser_list_cols <- c(by_cols, "varSeries")
  ser_list_df <- unique(graphTable[ser_list_cols])
  n_ser <- nrow(ser_list_df)
  
  
  if (n_ser == 0) {
    warning("Nothing to plot!\n", call. = FALSE, immediate. = TRUE)
  } else {
    
    if (is.null(pdf_file)) {
      display_flag <- FALSE
    } else {
      
      # Create the temporary (local) PDF file (landscape letter size format with 1 inch borders)
      tmp_pdf <- tempfile(fileext = ".pdf")
      grDevices::pdf(tmp_pdf, onefile = TRUE, width = 11, height = 8.5, encoding = "CP1250")
      # NOTE: encoding CP1250 is used to prevent a warning with the default encoding on Unix-like environments  
      #       (e.g., StatCan GitLab pipeline's Linux Unbuntu) regarding the replacement of characters "..." 
      #       (e.g., long `graphTable` data frame name) with the 'Horizontal Ellipsis' unicode character ("\u2026").
      if (grDevices::dev.cur() == 1) {
        warning("PDF file not created (`grDevices::pdf()` failed)!\n", call. = FALSE, immediate. = TRUE)
        display_flag <- FALSE
      } else {
        display_flag <- TRUE
      }
    }
    
    
    # Display a "starting" message
    message("\nGenerating the benchmarking graphics. Please be patient...\n")
    
    # ggplot2 initialization
    pt_sz <- 2
    ini_theme <- ggplot2::theme_get()
    intialize_theme()
    on.exit(ggplot2::theme_set(ini_theme), add = TRUE)
    
    # Initialize the PDF bookmarks data frame
    bookmarks_df <- data.frame(title = rep.int("", n_ser),
                               page = rep.int(NA_integer_, n_ser))
    
    # Assign the plotting function names according to each individual flag
    # (single "if" instead of repeated "ifs" for each series to plot)
    if (ori_plot_flag) {
      ori_plot_func <- ori_plot
    } else {
      ori_plot_func <- gs.NULL_func
    }
    if (adj_plot_flag) {
      adj_plot_func <- adj_plot
    } else {
      adj_plot_func <- gs.NULL_func
    }
    if (GR_plot_flag) {
      GR_plot_func <- GR_plot
    } else {
      GR_plot_func <- gs.NULL_func
    }
    if (GR_table_flag) {
      GR_table_func <- GR_table
    } else {
      GR_table_func <- gs.NULL_func
    }
    
    
    # Build the plot subtitles and PDF bookmark labels
    plot_subtitle <- rep.int(paste0("Graphics Table <strong>", mask_underscore(df_name), "</strong>"), n_ser)
    
    # Remove unnecessary columns (those with a single value for all rows of the graphTable data frame, if any)
    ser_list_cols <- ser_list_cols[lapply(lapply(ser_list_df, unique), length) > 1]
    if (length(ser_list_cols) == 0) {
      bookmarks_df$title <- rep.int("<Single Series>", n_ser)
      by_cols <- character(0L)
    } else {
      
      # "varSeries" column info
      if ("varSeries" %in% ser_list_cols) {
        bookmarks_df$title <- ser_list_df[["varSeries"]]
        plot_subtitle <- paste0(plot_subtitle, " - Variable <strong>", mask_underscore(bookmarks_df$title), "</strong>")
        label_sep <- " - "
      } else {
        label_sep <- ""
      }
      
      # By-group columns info
      by_cols <- setdiff(ser_list_cols, "varSeries")
      if (length(by_cols) > 0) {
        if (length(by_cols) == 1) {
          byGrp_label <- as.character(ser_list_df[[by_cols]])
        } else {
          # `sapply()` is safe: it always return a character vector (`by_cols` is a vector of minimum length 2)
          byGrp_label <- apply(sapply(by_cols, function(x) paste0(x, "=", ser_list_df[[x]])),
                               1, paste0, collapse = " & ")
        }
        bookmarks_df$title <- paste0(bookmarks_df$title, label_sep, byGrp_label)
        plot_subtitle <- paste0(plot_subtitle, " - BY-Group <strong>", mask_underscore(byGrp_label), "</strong>")
      }
    }
    
    
    # Reduce the data frame to essential columns 
    graphTable <- graphTable[c(by_cols, mandatory_cols)]
    
    
    # Process each series
    
    pdf_page <- 1
    for (ii in 1:n_ser) {
      
      # Set the PDF bookmark page
      bookmarks_df$page[ii] <- pdf_page
      
      # Initialize `graph_list` objects 
      out_list$graph_list <- c(out_list$graph_list, 
                               list(list(name = bookmarks_df$title[ii],
                                         page = bookmarks_df$page[ii],
                                         ggplot_list = NULL)))
      
      # Extract the relevant graphTable rows
      plot_df <- merge(ser_list_df[ii, ser_list_cols, drop = FALSE], 
                       graphTable, 
                       by = ser_list_cols)
      plot_df <- plot_df[order(plot_df$year, plot_df$period), ]
      
      # Extract the benchmarking parms
      parms <- extract_parms(plot_df)
      
      
      # Generate the plots
      
      subtitle_str <- paste0(plot_subtitle[ii], "<br>(", parms$parms_str, ")")
      
      tmp_list <- ori_plot_func(graphTable = plot_df,
                                subtitle_str = subtitle_str,
                                mth_gap = parms$mth_gap,
                                points_set = parms$ori_plot_set,
                                pt_sz = pt_sz,
                                display_ggplot = display_flag,
                                .setup = FALSE)
      out_list$graph_list[[ii]]$ggplot_list <- c(out_list$graph_list[[ii]]$ggplot_list, tmp_list)
      pdf_page <- pdf_page + length(tmp_list)
      
      tmp_list <- adj_plot_func(graphTable = plot_df,
                                subtitle_str = subtitle_str,
                                mth_gap = parms$mth_gap,
                                full_set = parms$adj_plot_set,
                                pt_sz = pt_sz,
                                display_ggplot = display_flag,
                                .setup = FALSE)
      out_list$graph_list[[ii]]$ggplot_list <- c(out_list$graph_list[[ii]]$ggplot_list, tmp_list)
      pdf_page <- pdf_page + length(tmp_list)
      
      tmp_list <- GR_plot_func(graphTable = plot_df,
                               subtitle_str = subtitle_str,
                               factor = parms$GR_factor,
                               type_chars = parms$GR_type_chars,
                               periodicity = parms$periodicity,
                               display_ggplot = display_flag,
                               .setup = FALSE)
      out_list$graph_list[[ii]]$ggplot_list <- c(out_list$graph_list[[ii]]$ggplot_list, tmp_list)
      pdf_page <- pdf_page + length(tmp_list)
      
      tmp_list <- GR_table_func(graphTable = plot_df,
                                subtitle_str = subtitle_str,
                                factor = parms$GR_factor,
                                type_chars = parms$GR_type_chars,
                                display_ggplot = display_flag,
                                .setup = FALSE)
      out_list$graph_list[[ii]]$ggplot_list <- c(out_list$graph_list[[ii]]$ggplot_list, tmp_list)
      pdf_page <- pdf_page + length(tmp_list)
      
    }
    
    
    if (display_flag) {
      
      # Close the temporary PDF file and add bookmarks
      grDevices::dev.off()
      if (add_bookmarks) {
        if (xmpdf::supports_set_bookmarks()) {
          tmp_pdf2 <- try(xmpdf::set_bookmarks(bookmarks_df, tmp_pdf, tempfile(fileext = ".pdf")))
          if (exists("tmp_pdf2") && file.exists(tmp_pdf2)) {
            unlink(tmp_pdf)
            tmp_pdf <- tmp_pdf2
            bookmark_msg_flag <- FALSE
          } else {
            bookmark_msg_flag <- TRUE
          }
        } else {
          bookmark_msg_flag <- TRUE
        }
      } else {
        bookmark_msg_flag <- FALSE
      }
      
      # Create the final (remote) PDF file
      out_pdf <- as.character((unlist(pdf_file))[1])
      if (length(out_pdf) > 0 && !is.na(out_pdf)) {
        len <- nchar(out_pdf)
        if (toupper(substr(out_pdf, len - 3, len)) != ".PDF") {
          out_pdf <- paste0(out_pdf, ".pdf")
        }
        # NOTE: moving a file with `file.copy()` + `unlink()` is safer than with `file.rename()` 
        #       (see section "Warning" in `help(file.rename)`)
        if (file.copy(from = tmp_pdf, to = out_pdf, overwrite = TRUE)) {
          unlink(tmp_pdf)
        } else {
          # `file.copy()` failed, most likely due to a "file locking issue"
          message("")
          out_pdf <- tmp_pdf
        }
      } else {
        warning("Invalid PDF file specification (argument 'pdf_file'). A temporary PDF file will be used instead.\n", 
                call. = FALSE, immediate. = TRUE)
        out_pdf <- tmp_pdf
      }
      
      # Completion message
      out_list$pdf_name <- utils::fileSnapshot(out_pdf)$path
      message("Benchmarking graphics generated for ", format(n_ser), " series in the following PDF file:\n  ",
              normalizePath(out_list$pdf_name), "\n")
      if (bookmark_msg_flag) {
        message("Bookmarks could not be added to the PDF file (see section \"Bookmarks\" in `help(plot_graphTable)`).\n")
      }
      
    } else {
      message("Benchmarking graphics generated for ", format(n_ser), " series.\n")
    }
  }
  
  # Output object returned via function `on.exit()`
}




#' Generate a benchmarking graphic
#' 
#' @name bench_graphs
#'
#' @description
#'
#' \if{html,text}{(\emph{version française: 
#' \url{https://StatCan.github.io/gensol-gseries/fr/reference/bench_graphs.html}})}
#' 
#' Functions used internally by [plot_graphTable()] to generate the benchmarking graphics in a PDF file:
#' - [ori_plot()]: Original Scale Plot ([plot_graphTable()] argument `ori_plot_flag = TRUE`)
#' - [adj_plot()]: Adjustment Scale Plot ([plot_graphTable()] argument `adj_plot_flag = TRUE`)
#' - [GR_plot()]: Growth Rates Plot ([plot_graphTable()] argument `GR_plot_flag = TRUE`)
#' - [GR_table()]: Growth Rates Table ([plot_graphTable()] argument `GR_table_flag = TRUE`)
#' 
#' When these functions are called directly, the `graphTable` data frame should only  contain a **single series** 
#' and the graphic is generated in the current (active) graphics device.
#' 
#' 
#' @inheritParams plot_graphTable
#' 
#' @param title_str,subtitle_str (optional)
#' 
#' Graphic title and subtitle strings (character constants). `subtitle_str` is automatically built from the 
#' `graphTable` data frame contents when `NULL` and contains the `graphTable` data frame name on the 2<sup>nd</sup> line 
#' and the benchmarking parameters on the 3<sup>rd</sup> line. Specifying empty strings (`""`) would remove the titles. 
#' Simple Markdown and HTML syntax is allowed (e.g., for bold, italic or colored fonts) through package [ggtext][ggtext::ggtext] 
#' (see `help(package = "ggtext")`).
#' 
#' **Default values** are `subtitle_str = NULL` and a function specific string for `title_str` (see **Usage**).
#' 
#' @param mth_gap (optional)
#' 
#' Number of months between consecutive periods (e.g. 1 for monthly data, 3 for quarterly data, etc.). Based  
#' on the `graphTable` data frame contents  when `NULL` (calculated as `12 / graphTable$periodicity[1]`).
#' 
#' **Default value** is `mth_gap = NULL`.
#' 
#' @param points_set,full_set (optional)
#' 
#' Character vector of the elements (variables of the `graphTable` data frame) to include in the plot.
#' Automatically built when `NULL`. See [plot_graphTable()] for the (default) list of variables used for each 
#' type of graphic.
#' 
#' **Default values** are `points_set = NULL` and `full_set = NULL`.
#' 
#' @param pt_sz (optional)
#' 
#' Size of the data points shape (symbol) for ggplot2.
#' 
#' **Default value** is `pt_sz = 2`.
#' 
#' @param factor,type_chars (optional)
#' 
#' Growth rates factor (1 or 100) and value label suffix ("" or "(%)") according to the adjustment model parameter 
#' \eqn{\lambda}. Based on the `graphTable` data frame contents when `NULL` (based on `graphTable$lambda[1]`).
#' 
#' **Default values** are `factor = NULL` and `type_chars = NULL`.
#' 
#' @param periodicity (optional)
#' 
#' Number of periods in a year. Based on the `graphTable` data frame contents when `NULL` (defined as 
#' `graphTable$periodicity[1]`).
#' 
#' **Default value** is `periodicity = NULL`.
#'
#' @param display_ggplot (optional)
#' 
#' Logical arguments indicating whether or not the ggplot object(s) should be displayed in the current (active) 
#' graphics device.
#'
#' **Default value** is `display_ggplot = TRUE`.
#'
#' @param .setup (optional)
#' 
#' Logical argument indicating whether the setup steps must be executed or not. Must be `TRUE` when the function 
#' is called directly (i.e., outside of the [plot_graphTable()] context). 
#' 
#' **Default value** is `.setup = TRUE`.
#'
#'
#' @details
#' See [plot_graphTable()] for a detailed description of the four benchmarking graphics associated to these 
#' individual functions. These graphics are optimized for the US Letter paper size format in landscape view, i.e., 
#' 11in wide (27.9cm, 1056px with 96 DPI) and 8.5in tall (21.6cm, 816px with 96 DPI). Keep this in mind when 
#' viewing or saving graphics generated by calls to these individual functions (i.e., outside of the [plot_graphTable()] 
#' context). Also note that [GR_plot()] and [GR_table()] will often generate more than one graphic (more than one 
#' *page*), unless the number of periods included in the input `graphTable` data frame is reduced (e.g., subsetting 
#' the data frame by ranges of calendar years). 
#'
#'
#' @returns
#' In addition to displaying the corresponding graphic(s) in the current (active) graphics device (except when 
#' `display_ggplot = FALSE`), each function also invisibly returns a list containing the generated ggplot object(s). 
#' Notes:
#' - [ori_plot()] and [adj_plot()] generate a single ggplot object (single graphic) while [GR_plot()] and [GR_table()] 
#' will often generate several ggplot objects (several graphics).
#' - The returned ggplot object(s) can be displayed _manually_ with [print()], in which case the following ggplot2 theme 
#' updates (used internally when `display_ggplot = TRUE`) are suggested:
#'   ```R
#'   ggplot2::theme_update(
#'     plot.title = ggtext::element_markdown(hjust = 0.5),
#'     plot.subtitle = ggtext::element_markdown(hjust = 0.5),
#'     legend.position = "bottom",
#'     plot.margin = ggplot2::margin(t = 1.5, r = 1.5, b = 1.5, l = 1.5, unit = "cm")
#'   )
#'   ```
#'
#'
#' @seealso [plot_graphTable()] [plot_benchAdj()] [benchmarking()] [stock_benchmarking()]
#'
#'
#' @example misc/function_examples/bench_graphs-ex.R
#' 
#' 
#' @importFrom rlang .data


#' @rdname bench_graphs
#' @export
ori_plot <- function(graphTable, 
                     title_str = "Original Scale",
                     subtitle_str = NULL, 
                     mth_gap = NULL, 
                     points_set = NULL,
                     pt_sz = 2,
                     display_ggplot = TRUE,
                     .setup = TRUE) {
  
  
  # Original Scale graph function (called when `ori_plot_flag = TRUE`)
  # Approach:
  #   - create 3 tall (stacked) data frames for plotting with ggplot:
  #       - data points (geom_point) for series values and repeated average values
  #       - lines (geom_line) for series lines only (not average lines)
  #       - segments (geom_segment) for average lines
  #   - generate "by name (series)" geoms for each type of element to plot
  #     (geom_line, geom_segment and geom_point)
  
  
  # Initialize the object to be returned by the function via `on.exit()`
  ggplot_list <- NULL
  on.exit(return(invisible(ggplot_list)))
  
  # Enforce the default R "error" option (`options(error = NULL)`). E.g. this Turns off traceback
  # generated by calls to the stop() function inside internal functions in R Studio.
  ini_error_opt <- getOption("error")
  on.exit(options(error = ini_error_opt), add = TRUE)
  options(error = NULL)
  
  # Set the display function
  if (gs.validate_arg_logi(display_ggplot)) {
    display_func <- print
  } else {
    display_func <- gs.NULL_func
  }
  
  # Setup part (manual call outside on `plot_graphTable()`)
  if (gs.validate_arg_logi(.setup)) {

    # Set PDF encoding to CP1250 to avoid warnings on Unix-like environments 
    # (see `plot_graphTable()` comment above)
    ini_pdf_encoding <- grDevices::pdf.options()$encoding
    on.exit(grDevices::pdf.options(encoding = ini_pdf_encoding), add = TRUE)
    grDevices::pdf.options(encoding = "CP1250")
    
    # ggplot2 initialization
    ini_theme <- ggplot2::theme_get()
    intialize_theme()
    on.exit(ggplot2::theme_set(ini_theme), add = TRUE)
    
    # Data frame name string
    df_name <- deparse1(substitute(graphTable))
    if (nchar(df_name) >= 60) {
      df_name <- paste0(substr(df_name, 1, 55), "<...>")
    }
    if (grepl("structure(", df_name, fixed = TRUE)) {
      df_name <- "<argument 'graphTable'>"
    }
    graphTable <- graphTable
    if (!is.data.frame(graphTable)) {
      stop("Argument 'graphTable' is not a 'data.frame' object.\n\n", call. = FALSE)
    }
    graphTable <- as.data.frame(graphTable)
    
    # Set the benchmarking parms
    parms <- extract_parms(graphTable)
    if (is.null(subtitle_str)) {
      subtitle_str <- paste0("Graphics Table <strong>", mask_underscore(df_name), "</strong><br>(", parms$parms_str, ")")
    }
    if (is.null(mth_gap)) {
      mth_gap <- parms$mth_gap
    }
    if (is.null(points_set)) {
      points_set <- parms$ori_plot_set
      segments_set <- c("avgSubAnnual", "avgBenchmark")
    } else {
      segments_set <- intersect(c("avgSubAnnual", "avgBenchmark"), points_set)
    }
    
    graphTable <- graphTable[order(graphTable$year, graphTable$period), ]
  } else {
    ini_theme <- NULL
    segments_set <- c("avgSubAnnual", "avgBenchmark")
  }
  lines_set <- setdiff(points_set, segments_set)
  
  # Create the points data frame for geom_point
  points_df <- stack_tsDF(graphTable[!is.na(graphTable[points_set[1]]), c("year", "period", points_set), drop = FALSE])
  points_df <- points_df[!duplicated(points_df), ]
  points_df$series <- factor(points_df$series, levels = points_set, ordered = TRUE)
  points_df$rDate <- as.Date(paste(points_df$year, 1 + (points_df$period - 1) * mth_gap, "1", sep = "-"), "%Y-%m-%d")
  
  # Extract rows corresponding to lines for geom_line
  lines_df <- points_df[points_df$series %in% lines_set, , drop = FALSE]
  
  # Create the segments data frame for geom_segment
  if (length(segments_set) > 0) {
    df_temp <- graphTable[!is.na(graphTable[segments_set[1]]), c("m", "year", "period", segments_set), drop = FALSE]
    m_vec <- unique(df_temp$m)
    M <- length(m_vec)
    df_beg <- data.frame(matrix(NA_real_, nrow = M, ncol = 2 + length(segments_set)))
    df_end <- df_beg
    names(df_beg) <- c("year", "period", segments_set)
    names(df_end) <- c("year_end", "period_end", segments_set)
    for (kk in m_vec) {
      df_beg[kk, ] <- utils::head(df_temp[df_temp$m == m_vec[kk], c("year", "period", segments_set)], n = 1)
      df_end[kk, ] <- utils::tail(df_temp[df_temp$m == m_vec[kk], c("year", "period", segments_set)], n = 1)
    }
    segments_df <- cbind(stack_tsDF(df_beg), stack_tsDF(df_end,
                                                        yr_cName = "year_end",
                                                        per_cName = "period_end",
                                                        val_cName = "value_end")[, -1])
    segments_df$series <- factor(segments_df$series, levels = segments_set, ordered = TRUE)
    segments_df$rDate <- as.Date(paste(segments_df$year, 1 + (segments_df$period - 1) * mth_gap, "1",
                                       sep = "-"), "%Y-%m-%d")
    segments_df$rDate_end <- as.Date(paste(segments_df$year_end, 1 + (segments_df$period_end - 1) * mth_gap, "1",
                                           sep = "-"), "%Y-%m-%d")
  } else {
    segments_df <- data.frame(rDate = numeric(0L),
                              value = numeric(0L),
                              rDate_end = numeric(0L),
                              value_end = numeric(0L),
                              series = character(0L))
  }
  
  labels <-    c("subAnnual"          = "Indicator Series",
                 "avgSubAnnual"       = "Avg. Indicator Series",
                 "subAnnualCorrected" = "Bias Corr. Indicator Series",
                 "benchmarked"        = "Benchmarked Series",
                 "avgBenchmark"       = "Average Benchmark")
  colors <-    c("subAnnual"          = "blue",
                 "avgSubAnnual"       = "blue",
                 "subAnnualCorrected" = "orange",
                 "benchmarked"        = "red",
                 "avgBenchmark"       = "red")
  pt_shapes <- c("subAnnual"          = "plus",
                 "avgSubAnnual"       = "diamond open",
                 "subAnnualCorrected" = "circle open",
                 "benchmarked"        = "asterisk",
                 "avgBenchmark"       = "triangle open")
  pt_sizes <-  c("subAnnual"          = pt_sz,
                 "avgSubAnnual"       = pt_sz,
                 "subAnnualCorrected" = pt_sz + 0.5,
                 "benchmarked"        = pt_sz,
                 "avgBenchmark"       = pt_sz)
  
  original_scale <- ggplot2::ggplot() +
    
    # Series lines
    ggplot2::geom_line(data = lines_df, ggplot2::aes(x = .data$rDate, y = .data$value,
                                                     color = .data$series)) +
    
    # Average lines (segments)
    ggplot2::geom_segment(data = segments_df, ggplot2::aes(x    = .data$rDate,     y    = .data$value,
                                                           xend = .data$rDate_end, yend = .data$value_end,
                                                           color = .data$series)) +
    
    # Data points for series and repeated average values
    ggplot2::geom_point(data = points_df, ggplot2::aes(x = .data$rDate, y = .data$value,
                                                       color = .data$series,
                                                       shape = .data$series,
                                                       size = .data$series)) +
    
    ggplot2::scale_color_manual(name = NULL, values = colors, limits = points_set, labels = labels) +
    ggplot2::scale_shape_manual(name = NULL, values = pt_shapes, limits = points_set, labels = labels) +
    ggplot2::scale_size_manual(name = NULL, values = pt_sizes, limits = points_set, labels = labels) +
    ggplot2::scale_x_date(name = NULL, date_labels = "%Y", breaks = "1 year", minor_breaks = NULL) +
    ggplot2::labs(title = title_str, subtitle = subtitle_str, y = "Original Scale")
  
  ggplot_list <- list(original_scale)
  display_func(original_scale)

  # Output object returned via function `on.exit()`
}


#' @rdname bench_graphs
#' @export
adj_plot <- function(graphTable, 
                     title_str = "Adjustment Scale",
                     subtitle_str = NULL, 
                     mth_gap = NULL, 
                     full_set = NULL,
                     pt_sz = 2,
                     display_ggplot = TRUE,
                     .setup = TRUE) {
  
  
  # Adjustment Scale graph function (called when `adj_plot_flag = TRUE`)
  # Approach:
  #   - create 3 tall (stacked) data frames for plotting with ggplot:
  #       - data points (geom_point) for adjustment values and repeated average adj. values
  #         (no bias values)
  #       - lines (geom_line) for adjustment and bias lines (not average adj. lines)
  #       - segments (geom_segment) for average adj. lines
  #   - generate "by name (series)" geoms for each type of element to plot
  #     (geom_line, geom_segment and geom_point)
  
  
  # Initialize the object to be returned by the function via `on.exit()`
  ggplot_list <- NULL
  on.exit(return(invisible(ggplot_list)))
  
  # Enforce the default R "error" option (`options(error = NULL)`). E.g. this Turns off traceback
  # generated by calls to the stop() function inside internal functions in R Studio.
  ini_error_opt <- getOption("error")
  on.exit(options(error = ini_error_opt), add = TRUE)
  options(error = NULL)
  
  # Set the display function
  if (gs.validate_arg_logi(display_ggplot)) {
    display_func <- print
  } else {
    display_func <- gs.NULL_func
  }
  
  # Setup part (manual call outside on `plot_graphTable()`)
  if (gs.validate_arg_logi(.setup)) {

    # Set PDF encoding to CP1250 to avoid warnings on Unix-like environments 
    # (see `plot_graphTable()` comment above)
    ini_pdf_encoding <- grDevices::pdf.options()$encoding
    on.exit(grDevices::pdf.options(encoding = ini_pdf_encoding), add = TRUE)
    grDevices::pdf.options(encoding = "CP1250")
    
    # ggplot2 initialization
    ini_theme <- ggplot2::theme_get()
    intialize_theme()
    on.exit(ggplot2::theme_set(ini_theme), add = TRUE)
    
    # Data frame name string
    df_name <- deparse1(substitute(graphTable))
    if (nchar(df_name) >= 60) {
      df_name <- paste0(substr(df_name, 1, 55), "<...>")
    }
    if (grepl("structure(", df_name, fixed = TRUE)) {
      df_name <- "<argument 'graphTable'>"
    }
    graphTable <- graphTable
    if (!is.data.frame(graphTable)) {
      stop("Argument 'graphTable' is not a 'data.frame' object.\n\n", call. = FALSE)
    }
    graphTable <- as.data.frame(graphTable)
    
    # Set the benchmarking parms
    parms <- extract_parms(graphTable)
    if (is.null(subtitle_str)) {
      subtitle_str <- paste0("Graphics Table <strong>", mask_underscore(df_name), "</strong><br>(", parms$parms_str, ")")
    }
    if (is.null(mth_gap)) {
      mth_gap <- parms$mth_gap
    }
    if (is.null(full_set)) {
      full_set <- parms$adj_plot_set
      segments_set <- "avgBenchmarkSubAnnualRatio"
    } else {
      segments_set <- intersect("avgBenchmarkSubAnnualRatio", full_set)
    }
    
    graphTable <- graphTable[order(graphTable$year, graphTable$period), ]
  } else {
    ini_theme <- NULL
    segments_set <- "avgBenchmarkSubAnnualRatio"
  }
  lines_set <- setdiff(full_set, segments_set)
  points_set <- setdiff(full_set, "bias")
  
  # Create the values data frame for lines (geom_lines) and points (geom_point)
  temp_set <- intersect(full_set, union(lines_set, points_set))
  values_df <- stack_tsDF(graphTable[!is.na(graphTable[lines_set[1]]), c("year", "period", temp_set), drop = FALSE])
  values_df <- values_df[!duplicated(values_df), ]
  values_df$series <- factor(values_df$series, levels = temp_set, ordered = TRUE)
  values_df$rDate <- as.Date(paste(values_df$year, 1 + (values_df$period - 1) * mth_gap, "1", sep = "-"), "%Y-%m-%d")
  
  # Extract rows corresponding to lines for geom_line
  lines_df <- values_df[values_df$series %in% lines_set, , drop = FALSE]
  
  # Extract rows corresponding to points for geom_point
  points_df <- values_df[values_df$series %in% points_set, , drop = FALSE]
  
  # Create the segments data frame for geom_segment
  if (length(segments_set) > 0) {
    df_temp <- graphTable[!is.na(graphTable[segments_set[1]]), c("m", "year", "period", segments_set), drop = FALSE]
    m_vec <- unique(df_temp$m)
    M <- length(m_vec)
    df_beg <- data.frame(matrix(NA_real_, nrow = M, ncol = 2 + length(segments_set)))
    df_end <- df_beg
    names(df_beg) <- c("year", "period", segments_set)
    names(df_end) <- c("year_end", "period_end", segments_set)
    for (kk in m_vec) {
      df_beg[kk, ] <- utils::head(df_temp[df_temp$m == m_vec[kk], c("year", "period", segments_set)], n = 1)
      df_end[kk, ] <- utils::tail(df_temp[df_temp$m == m_vec[kk], c("year", "period", segments_set)], n = 1)
    }
    segments_df <- cbind(stack_tsDF(df_beg), stack_tsDF(df_end,
                                                        yr_cName = "year_end",
                                                        per_cName = "period_end",
                                                        val_cName = "value_end")[, -1])
    segments_df$series <- factor(segments_df$series, levels = segments_set, ordered = TRUE)
    segments_df$rDate <- as.Date(paste(segments_df$year, 1 + (segments_df$period - 1) * mth_gap, "1",
                                       sep = "-"), "%Y-%m-%d")
    segments_df$rDate_end <- as.Date(paste(segments_df$year_end, 1 + (segments_df$period_end - 1) * mth_gap, "1",
                                           sep = "-"), "%Y-%m-%d")
  } else {
    segments_df <- data.frame(rDate = numeric(0L),
                              value = numeric(0L),
                              rDate_end = numeric(0L),
                              value_end = numeric(0L),
                              series = character(0L))
  }
  
  labels <-    c("benchmarkedSubAnnualRatio"  = "BI Ratios (Benchmarked Series / Indicator Series)",
                 "avgBenchmarkSubAnnualRatio" = "Avgerage BI Ratios",
                 "bias"                       = "Bias")
  colors <-    c("benchmarkedSubAnnualRatio"  = "blue",
                 "avgBenchmarkSubAnnualRatio" = "black",
                 "bias"                       = "green4")
  ln_types <-  c("benchmarkedSubAnnualRatio"  = "solid",
                 "avgBenchmarkSubAnnualRatio" = "solid",
                 "bias"                       = "longdash")
  pt_shapes <- c("benchmarkedSubAnnualRatio"  = "plus",
                 "avgBenchmarkSubAnnualRatio" = "triangle open",
                 "bias"                       = "")
  pt_sizes  <- c("benchmarkedSubAnnualRatio"  = pt_sz,
                 "avgBenchmarkSubAnnualRatio" = pt_sz,
                 "bias"                       = 0)
  
  adjustment_scale <- ggplot2::ggplot() +
    
    # Adjustment and bias lines
    ggplot2::geom_line(data = lines_df, ggplot2::aes(x = .data$rDate, y = .data$value,
                                                     color = .data$series,
                                                     linetype = .data$series)) +
    
    # Average adjustments (segments)
    ggplot2::geom_segment(data = segments_df, ggplot2::aes(x    = .data$rDate,     y    = .data$value,
                                                           xend = .data$rDate_end, yend = .data$value_end,
                                                           color = .data$series,
                                                           linetype = .data$series)) +
    
    # Data points for adjustment values and repeated average adj. values
    ggplot2::geom_point(data = points_df, ggplot2::aes(x = .data$rDate, y = .data$value,
                                                       color = .data$series,
                                                       shape = .data$series,
                                                       size = .data$series)) +
    
    ggplot2::scale_color_manual(name = NULL, values = colors, limits = full_set, labels = labels) +
    ggplot2::scale_linetype_manual(name = NULL, values = ln_types, limits = full_set, labels = labels) +
    ggplot2::scale_shape_manual(name = NULL, values = pt_shapes, limits = full_set, labels = labels) +
    ggplot2::scale_size_manual(name = NULL, values = pt_sizes, limits = full_set, labels = labels) +
    ggplot2::scale_x_date(name = NULL, date_labels = "%Y", breaks = "1 year", minor_breaks = NULL) +
    ggplot2::labs(title = title_str, subtitle = subtitle_str, y = "Adjustment Scale")
  
  ggplot_list <- list(adjustment_scale)
  display_func(adjustment_scale)

  # Output object returned via function `on.exit()`
}


#' @rdname bench_graphs
#' @export
GR_plot <- function(graphTable, 
                    title_str = "Growth Rates",
                    subtitle_str = NULL, 
                    factor = NULL, 
                    type_chars = NULL, 
                    periodicity = NULL,
                    display_ggplot = TRUE,
                    .setup = TRUE) {
  
  
  # Growth Rates graph function (called when `GR_plot_flag = TRUE`)
  # Approach:
  #   - create a tall (stacked) data frame for plotting with ggplot
  #   - generate a "by name (series)" geom_col
  #   - use geom_col position = "dodge" to stick the 2 GR bars (before/after) together
  #   - use facet_wrap to group the GR bars by year
  #   - create separate graphs (PDF pages) depending on the number of years and the periodicity
  #     (4 years per page for quarterly data, 2 years per page for monthly data)
  #   - use the same X and Y scales (1 to # of periods and GR min/max values) to ensure
  #     proper visual comparability across all pages
  #   - complete the last page with "empty years" to ensure the same look for all years
  #     (same yearly graph width across all pages)

 
  # Initialize the object to be returned by the function via `on.exit()`
  ggplot_list <- NULL
  on.exit(return(invisible(ggplot_list)))
  
  # Enforce the default R "error" option (`options(error = NULL)`). E.g. this Turns off traceback
  # generated by calls to the stop() function inside internal functions in R Studio.
  ini_error_opt <- getOption("error")
  on.exit(options(error = ini_error_opt), add = TRUE)
  options(error = NULL)
  
  # Set the display function
  if (gs.validate_arg_logi(display_ggplot)) {
    display_func <- print
  } else {
    display_func <- gs.NULL_func
  }
  
  # Setup part (manual call outside on `plot_graphTable()`)
  if (gs.validate_arg_logi(.setup)) {

    # Set PDF encoding to CP1250 to avoid warnings on Unix-like environments 
    # (see `plot_graphTable()` comment above)
    ini_pdf_encoding <- grDevices::pdf.options()$encoding
    on.exit(grDevices::pdf.options(encoding = ini_pdf_encoding), add = TRUE)
    grDevices::pdf.options(encoding = "CP1250")
    
    # ggplot2 initialization
    ini_theme <- ggplot2::theme_get()
    intialize_theme()
    on.exit(ggplot2::theme_set(ini_theme), add = TRUE)
    
    # Data frame name string
    df_name <- deparse1(substitute(graphTable))
    if (nchar(df_name) >= 60) {
      df_name <- paste0(substr(df_name, 1, 55), "<...>")
    }
    if (grepl("structure(", df_name, fixed = TRUE)) {
      df_name <- "<argument 'graphTable'>"
    }
    graphTable <- graphTable
    if (!is.data.frame(graphTable)) {
      stop("Argument 'graphTable' is not a 'data.frame' object.\n\n", call. = FALSE)
    }
    graphTable <- as.data.frame(graphTable)
    
    # Set the benchmarking parms
    parms <- extract_parms(graphTable)
    if (is.null(subtitle_str)) {
      subtitle_str <- paste0("Graphics Table <strong>", mask_underscore(df_name), "</strong><br>(", parms$parms_str, ")")
    }
    if (is.null(factor)) {
      factor <- parms$GR_factor
    }
    if (is.null(type_chars)) {
      type_chars <- parms$GR_type_chars
    }
    if (is.null(periodicity)) {
      periodicity <- parms$periodicity
    }
    
    graphTable <- graphTable[order(graphTable$year, graphTable$period), ]
  } else {
    ini_theme <- NULL
  }
  bars_set <- c("growthRateSubAnnual", "growthRateBenchmarked")
  temp_df <- stack_tsDF(graphTable[!duplicated(graphTable[c("year", "period")]), c("year", "period", bars_set)])
  temp_df$series <- factor(temp_df$series, levels = bars_set, ordered = TRUE)
  temp_df$value <- temp_df$value * factor
  y_lab <- paste("Growth Rates", type_chars)
  
  labels <- c("growthRateSubAnnual"   = "Growth R. in Indicator Series",
              "growthRateBenchmarked" = "Growth R. in Benchmarked Series")
  colors <- c("growthRateSubAnnual"   = "steelblue",
              "growthRateBenchmarked" = "indianred3")
  y_limits <- c(min(temp_df$value, na.rm = TRUE), max(temp_df$value, na.rm = TRUE))
  
  years <- graphTable[!is.na(graphTable[bars_set[1]]), "year"]
  beg_yr <- min(years)
  end_yr <- max(years)
  step <- ceiling(16 / periodicity)
  
  # Template plot object
  p <- ggplot2::ggplot() +
    ggplot2::scale_fill_manual(name = NULL, values = colors, labels = labels) +
    ggplot2::scale_x_discrete(name = NULL, limits = as.character(1:periodicity)) +
    ggplot2::scale_y_continuous(name = y_lab, limits = y_limits) +
    ggplot2::labs(title = title_str, subtitle = subtitle_str)
  
  seq_beg_yr <- seq(beg_yr, end_yr, step)
  for (kk in seq_beg_yr) {
    end <- kk + step - 1
    bars_df <- temp_df[temp_df$year >= kk & temp_df$year <= end, , drop = FALSE]
    
    # Add missing panels (years) at the end when necessary (more than 1 row/page of GR plots
    # and the last row/page is not full/complete)
    if (kk > beg_yr && end_yr < end) {
      n_xtra_yrs <- end - end_yr
      bars_df <- rbind(bars_df, data.frame(year = rep((end_yr + 1):end, each = 2),
                                           period = rep(1, n_xtra_yrs * 2),
                                           series = rep(bars_set, n_xtra_yrs),
                                           value = rep(NA_real_, n_xtra_yrs * 2)))
    }
    
    # Create the plot object from the template
    gRates_plot <- p +
      ggplot2::geom_col(data = bars_df, ggplot2::aes(x = .data$period, y = .data$value,
                                                     fill = .data$series),
                        position = "dodge", na.rm = TRUE) +
      ggplot2::facet_wrap(~year, ncol = step)
    
    ggplot_list <- c(ggplot_list, list(gRates_plot))
    display_func(gRates_plot)
  }
  
  # Output object returned via function `on.exit()`
}


#' @rdname bench_graphs
#' @export
GR_table <- function(graphTable, 
                     title_str = "Growth Rates Table",
                     subtitle_str = NULL, 
                     factor = NULL, 
                     type_chars = NULL,
                     display_ggplot = TRUE,
                     .setup = TRUE) {
  
  
  # Growth Rates table function (called when `GR_table_flag = TRUE`)
  # Approach:
  #   - use ggplot to generate page titles that look the same as the previous pages (plots)
  #   - generate a dummy geom (geom_blank) to define an invisible grid to which is attached the
  #     GR table with a tableGrob object (gridExtra package) in an annotation_custom geom
  #   - use element_blank() in the ggplot theme in order to draw nothing else but the GR table
  #     (invisible grid)
  #   - generate as many plots (PDF pages) as necessary in order to display the entire GR table
  #     (21 periods per page, i.e. 23 rows in total including 2 rows for the table header)
  
  
  # Initialize the object to be returned by the function via `on.exit()`
  ggplot_list <- NULL
  on.exit(return(invisible(ggplot_list)))
  
  # Enforce the default R "error" option (`options(error = NULL)`). E.g. this Turns off traceback
  # generated by calls to the stop() function inside internal functions in R Studio.
  ini_error_opt <- getOption("error")
  on.exit(options(error = ini_error_opt), add = TRUE)
  options(error = NULL)
  
  # Set the display function
  if (gs.validate_arg_logi(display_ggplot)) {
    display_func <- print
  } else {
    display_func <- gs.NULL_func
  }
  
  # Setup part (manual call outside on `plot_graphTable()`)
  if (gs.validate_arg_logi(.setup)) {

    # Set PDF encoding to CP1250 to avoid warnings on Unix-like environments 
    # (see `plot_graphTable()` comment above)
    ini_pdf_encoding <- grDevices::pdf.options()$encoding
    on.exit(grDevices::pdf.options(encoding = ini_pdf_encoding), add = TRUE)
    grDevices::pdf.options(encoding = "CP1250")
    
    # ggplot2 initialization
    ini_theme <- ggplot2::theme_get()
    intialize_theme()
    on.exit(ggplot2::theme_set(ini_theme), add = TRUE)
    
    # Data frame name string
    df_name <- deparse1(substitute(graphTable))
    if (nchar(df_name) >= 60) {
      df_name <- paste0(substr(df_name, 1, 55), "<...>")
    }
    if (grepl("structure(", df_name, fixed = TRUE)) {
      df_name <- "<argument 'graphTable'>"
    }
    graphTable <- graphTable
    if (!is.data.frame(graphTable)) {
      stop("Argument 'graphTable' is not a 'data.frame' object.\n\n", call. = FALSE)
    }
    graphTable <- as.data.frame(graphTable)
    
    # Set the benchmarking parms
    parms <- extract_parms(graphTable)
    if (is.null(subtitle_str)) {
      subtitle_str <- paste0("Graphics Table <strong>", mask_underscore(df_name), "</strong><br>(", parms$parms_str, ")")
    }
    if (is.null(factor)) {
      factor <- parms$GR_factor
    }
    if (is.null(type_chars)) {
      type_chars <- parms$GR_type_chars
    }
    
    graphTable <- graphTable[order(graphTable$year, graphTable$period), ]
  } else {
    ini_theme <- NULL
  }
  
  # Extract the GR table info
  GR_df <- graphTable[!duplicated(graphTable[c("year", "period")]),
                      c("year", "period", "subAnnual", "benchmarked", "growthRateSubAnnual", "growthRateBenchmarked")]
  row.names(GR_df) <- NULL
  GR_df$growthRateSubAnnual <- GR_df$growthRateSubAnnual * factor
  GR_df$growthRateBenchmarked <- GR_df$growthRateBenchmarked * factor
  names(GR_df) <- c("Year", "Period", "Indicator\nSeries", "Benchmarked\nSeries",
                    paste("Growth Rate in\nIndicator Series", type_chars),
                    paste("Growth Rate in \nBenchmarked Series", type_chars))
  # Apply default formatting with commas (thousands separator) to all columns except "Year" and "Period"
  GR_df <- cbind(GR_df[1:2], format(GR_df[-(1:2)], big.mark = ","))
  
  # 22 or even 23 rows (periods) would actually fit on a page, but 21 rows looks better: same
  # location for the top of the table on all pages (full set of 21 periods or fewer periods)
  max_rows <- 21
  max_y <- max_rows + 2
  grid_df <- data.frame(x = 1, y = 1:max_y)
  n_rows <- nrow(GR_df)
  n_pages <- ceiling(n_rows / max_rows)
  
  # Template plot object
  p <- ggplot2::ggplot(grid_df, ggplot2::aes(x = .data$x, y = .data$y)) +
    ggplot2::geom_blank() +
    ggplot2::labs(title = title_str, subtitle = subtitle_str) +
    ggplot2::theme(legend.position = "none",
                   axis.title = ggplot2::element_blank(),
                   axis.text = ggplot2::element_blank(),
                   axis.ticks = ggplot2::element_blank(),
                   axis.line = ggplot2::element_blank(),
                   panel.background = ggplot2::element_blank(),
                   panel.border = ggplot2::element_blank(),
                   panel.grid = ggplot2::element_blank())
  
  for (kk in 1:n_pages) {
    id_vec <- (1 + (kk - 1) * max_rows):min(n_rows, kk * max_rows)
    
    # Create the plot object from the template
    gRates_table <- p +
      ggplot2::annotation_custom(gridExtra::tableGrob(GR_df[id_vec, , drop = FALSE], rows = NULL), 
                                 ymin = max_rows - length(id_vec) + 1, ymax = max_y) 
    
    ggplot_list <- c(ggplot_list, list(gRates_table))
    display_func(gRates_table)
  }
  
  # Output object returned via function `on.exit()`
}




#' Initialize the ggplot2 theme for the benchmarking graphics
#' @noRd
intialize_theme <- function() {
  
  # IMPORTANT: keep roxygen2 @return tag up to date with modifications made here!
  
  ggplot2::theme_update(
    plot.title = ggtext::element_markdown(hjust = 0.5),
    plot.subtitle = ggtext::element_markdown(hjust = 0.5),
    legend.position = "bottom",
    plot.margin = ggplot2::margin(t = 1.5, r = 1.5, b = 1.5, l = 1.5, unit = "cm"))
  
  invisible(NULL)
}




#' Extract benchmarking parms for the plots 
#' @noRd
extract_parms <- function(graphTable) {
  
  periodicity <- graphTable$periodicity[1]
  lambda <- graphTable$lambda[1]
  rho <- graphTable$rho[1]
  bias <- graphTable$bias[1]
  constant <- graphTable$constant[1]
  
  # Define the subtitle and plot elements
  # (bias-related info not displayed for Denton benchmarking (`rho = 1`))
  if (abs(rho - 1) > gs.tolerance) {
    parms_str <- paste0("Lambda = ", format(lambda), ", Rho = ", format(rho), ", Bias = ", format(bias))
    ori_plot_set <- c("subAnnual", "avgSubAnnual", "subAnnualCorrected", "benchmarked", "avgBenchmark")
    adj_plot_set <- c("benchmarkedSubAnnualRatio", "avgBenchmarkSubAnnualRatio", "bias")
  } else {
    parms_str <- paste0("Lambda = ", format(lambda), ", Rho = ", format(rho))
    ori_plot_set <- c("subAnnual", "avgSubAnnual", "benchmarked", "avgBenchmark")
    adj_plot_set <- c("benchmarkedSubAnnualRatio", "avgBenchmarkSubAnnualRatio")
  }
  if (abs(constant) > gs.tolerance) {
    parms_str <- paste0(parms_str, ", Constant = ", format(constant))
  }
  
  # Growth rates scale factor (100 or 1) and type (percent or not) for the plot labels
  # and table headers according to lambda
  if (lambda == 0) {
    GR_factor <- 1
    GR_type_chars <- ""
  } else {
    GR_factor <- 100
    GR_type_chars <- "(%)"
  }
  
  # Return the list of parms
  list(
    periodicity = periodicity,
    mth_gap = 12 / periodicity,
    ori_plot_set = ori_plot_set,
    adj_plot_set = adj_plot_set,
    parms_str = parms_str,
    GR_factor = GR_factor,
    GR_type_chars = GR_type_chars)
}




#' Mask undersores (_) for Markdown markup (prevent them from triggering italic formatting)
#' @noRd
mask_underscore <- function(str) {
  gsub("_", "\\\\_", str)
}
