#' @export
#' @title Prevent page breaks inside a flextable
#' @description
#' Prevents breaks between tables rows you want to stay together.
#' This feature only applies to Word and RTF output.
#' @inheritParams args_x_only
#' @param init init value for keep_with_next property, it default
#' value is `get_flextable_defaults()$keep_with_next`.
#' @param hdr_ftr if TRUE (default), prevent breaks between table body
#' and header and between table body and footer.
#' @param group name of a column to use for finding groups (when
#'   `group_def` is `"rle"` or `"nonempty"`) or an integer vector
#'   of row indices that start new groups (when `group_def` is `"starts"`).
#' @param group_def algorithm to be used to identify groups
#' that should not be split into two pages, one of 'rle', 'nonempty',
#' 'starts':
#'
#' - 'rle': runs of equal values are used to define the groups,
#' to be used with [tabulator()].
#' - 'nonempty': non empty value start a new group,
#' to be used with [as_flextable.tabular()].
#' - 'starts': `group` is a numeric vector of body row indices
#' where new groups begin. Page breaks are allowed before these rows.
#' @return updated flextable object
#' @details
#' The pagination of tables allows you to control their position
#' in relation to page breaks.
#'
#' For small tables, a simple setting is usually used that indicates
#' that all rows should be displayed together:
#'
#' ```
#' paginate(x, init = TRUE, hdr_ftr = TRUE)
#' ```
#'
#' For large tables, it is recommended to use a setting that
#' indicates that all rows of the header should be bound to
#' the first row of the table to avoid the case where the header
#' is displayed alone at the bottom of the page and then repeated
#' on the next one:
#'
#' ```
#' paginate(x, init = FALSE, hdr_ftr = TRUE)
#' ```
#'
#' For tables that present groups that you don't want to be presented
#' on two pages, you must use a parameterization involving the notion
#' of group and an algorithm for determining the groups.
#'
#'
#' ```
#' paginate(x, group = "grp", group_def = "rle")
#' ```
#'
#' @examples
#' \dontshow{
#' data.table::setDTthreads(1)
#' }
#' library(data.table)
#' library(flextable)
#'
#' init_flextable_defaults()
#'
#' multi_fun <- function(x) {
#'   list(mean = mean(x), sd = sd(x))
#' }
#'
#' dat <- as.data.table(ggplot2::diamonds)
#' dat <- dat[clarity %in% c("I1", "SI1", "VS2")]
#'
#' dat <- dat[, unlist(lapply(.SD, multi_fun),
#'   recursive = FALSE
#' ),
#' .SDcols = c("z", "y"),
#' by = c("cut", "color", "clarity")
#' ]
#'
#' tab <- tabulator(
#'   x = dat, rows = c("cut", "color"),
#'   columns = "clarity",
#'   `z stats` = as_paragraph(as_chunk(fmt_avg_dev(z.mean, z.sd, digit2 = 2))),
#'   `y stats` = as_paragraph(as_chunk(fmt_avg_dev(y.mean, y.sd, digit2 = 2)))
#' )
#'
#' ft_1 <- as_flextable(tab)
#' ft_1 <- autofit(x = ft_1, add_w = .05)
#' ft_1 <- paginate(ft_1, group = "cut", group_def = "rle")
#'
#' save_as_docx(ft_1, path = tempfile(fileext = ".docx"))
#' save_as_rtf(ft_1, path = tempfile(fileext = ".rtf"))
#' @seealso [knit_print.flextable()]
#' @family table_structure
paginate <- function(
  x,
  init = NULL,
  hdr_ftr = TRUE,
  group = character(),
  group_def = c("rle", "nonempty", "starts")
) {
  if (is.null(init)) {
    init <- get_flextable_defaults()$keep_with_next
  }
  x <- keep_with_next(x, value = init, part = "all")

  if (hdr_ftr) {
    x <- paginate_hdr_ftr(x)
  }

  g_algo <- match.arg(group_def, several.ok = FALSE)
  if ("starts" %in% g_algo) {
    if (is.numeric(group) && length(group) > 0L) {
      x <- paginate_starts(x = x, starts = group)
    }
  } else if (length(group) == 1 && is.character(group)) {
    if ("nonempty" %in% g_algo) {
      x <- paginate_nonempty(x = x, group = group)
    } else if ("rle" %in% g_algo) {
      x <- paginate_rleid(x = x, group = group)
    }
  }

  x
}

paginate_hdr_ftr <- function(x) {
  nrow_footer <- nrow_part(x, "footer")
  nrow_header <- nrow_part(x, "header")
  if (nrow_part(x, "header")) {
    x <- keep_with_next(x, value = TRUE, part = "header")
  }
  if (nrow_footer > 0 && nrow_part(x, "body")) {
    x <- keep_with_next(
      x,
      value = TRUE,
      part = "body",
      i = nrow_part(x, "body")
    )
  }
  if (nrow_footer > 0) {
    x <- keep_with_next(x, value = TRUE, part = "footer")
    x <- keep_with_next(x, i = nrow_footer, value = FALSE, part = "footer")
  }
  x
}

paginate_nonempty <- function(x, group) {
  if (nrow_part(x, "body") < 1) {
    return(x)
  }

  if (!group %in% colnames(x$body$dataset)) {
    stop("could not find ", shQuote(group))
  }
  grp_val <- x$body$dataset[[group]]
  index_false <- c(
    which(!is.na(grp_val) & !grp_val %in% ""),
    length(grp_val) + 1
  )
  index_false <- index_false - 1
  index_false <- index_false[index_false > 0]
  x <- keep_with_next(x, value = TRUE, part = "body")
  x <- keep_with_next(x, i = index_false, value = FALSE, part = "body")
  x
}

paginate_rleid <- function(x, group) {
  if (nrow_part(x, "body") < 1) {
    return(x)
  }

  if (!group %in% colnames(x$body$dataset)) {
    stop("could not find ", shQuote(group))
  }
  grp_val <- x$body$dataset[[group]]
  index_false <- as.integer(tapply(
    seq_along(grp_val),
    rleid(grp_val),
    function(z) z[length(z)]
  ))
  x <- keep_with_next(x, value = TRUE, part = "body")
  x <- keep_with_next(x, i = index_false, value = FALSE, part = "body")
  x
}

paginate_starts <- function(x, starts) {
  n <- nrow_part(x, "body")
  if (n < 1L) {
    return(x)
  }

  index_false <- starts - 1L
  index_false <- index_false[index_false > 0L & index_false <= n]
  index_false <- unique(c(index_false, n))

  x <- keep_with_next(x, value = TRUE, part = "body")
  x <- keep_with_next(x, i = index_false, value = FALSE, part = "body")
  x
}
