
# grid_data ---------------------------------------------------------------

#' @importFrom data.table data.table rbindlist rleid .SD .N first fcase fifelse
get_grid_data <- function(x, autowidths, wrapping) {
  parts <- c("header", "body", "footer")
  names(parts) <- parts
  grid_data <- rbindlist(lapply(parts, function(part) {
    nr <- nrow_part(x, part)
    if (nr > 0) {
      data.frame(
        col_id = rep(x$col_keys, each = nr),
        ft_row_id = rep(seq_len(nr), length(x$col_keys)),
        stringsAsFactors = FALSE,
        check.names = FALSE
      )
    }
  }), use.names = TRUE, idcol = "part")

  grid_data$part <- factor(grid_data$part, levels = intersect(parts, grid_data$part))
  grid_data$col_id <- factor(grid_data$col_id, levels = x$col_keys)
  keycols <- c("part", "ft_row_id", "col_id")
  setorderv(grid_data, cols = keycols)
  grid_data[, c("row_index", "col_index") := list(
    rleid(.SD$part, .SD$ft_row_id), as.integer(.SD$col_id)
  )]
  setorderv(grid_data, cols = c("row_index", "col_index"))

  # add column widths
  col_widths <- fortify_width(x)
  colnames(col_widths)[colnames(col_widths) == "width"] <- "col_width"
  grid_data <- merge(grid_data, col_widths, by = c("col_id"))

  # add row heights
  row_heights <- fortify_height(x)
  colnames(row_heights)[colnames(row_heights) == "height"] <- "row_height"
  grid_data <- merge(grid_data, row_heights, by = c("part", "ft_row_id"))

  # add hrule
  row_hrules <- fortify_hrule(x)
  grid_data <- merge(grid_data, row_hrules, by = c("part", "ft_row_id"))

  # calculate minimum and maximum cell height according to hrule
  grid_data[, "row_min_height" := fcase(
    .SD$hrule %in% "exact", .SD$row_height,
    .SD$hrule %in% "atleast", .SD$row_height,
    default = NA_real_
  )]
  grid_data[, "row_max_height" := fcase(
    .SD$hrule %in% "exact", .SD$row_height,
    default = NA_real_
  )]

  # add spans
  spans <- fortify_span(x)
  grid_data <- merge(grid_data, spans, by = keycols)
  # create span groups
  setorderv(grid_data, cols = c("row_index", "col_index"))
  grid_data[, "rowspan_group" := calc_grid_span_group(.SD$rowspan)]
  setorderv(grid_data, cols = c("col_index", "row_index"))
  grid_data[, "colspan_group" := calc_grid_span_group(.SD$colspan)]

  # set cell width and height
  grid_data[, "cell_width" := safe_stat(.SD$col_width,
    FUN = sum, NA_value = NA_real_
  ), by = "rowspan_group"]
  grid_data[, "cell_height" := safe_stat(.SD$row_height,
    FUN = sum, NA_value = NA_real_
  ), by = "colspan_group"]
  grid_data[, "cell_min_height" := safe_stat(.SD$row_min_height,
    FUN = sum, NA_value = NA_real_
  ), by = "colspan_group"]
  grid_data[, "cell_max_height" := safe_stat_ext(.SD$row_max_height,
    FUN = sum, LENGTH = max(.SD$colspan), NA_value = NA_real_
  ), by = "colspan_group"]
  grid_data[, "cell_height" := safe_stat(
    safe_stat(.SD$cell_height, .SD$cell_min_height, FUN = max, NA_value = NA_real_),
    .SD$cell_max_height,
    FUN = min, NA_value = NA_real_
  ), by = c("col_index", "row_index")]

  # keep only active cells
  grid_data <- grid_data[grid_data$rowspan > 0 & grid_data$colspan > 0, , drop = FALSE]

  # generate argument list for creating the cell viewports
  setorderv(grid_data, cols = c("row_index", "col_index"))
  grid_data[, "cell_vp" := mapply(
    function(row, colspan, column, rowspan) {
      list(
        layout.pos.row = seq(row, length.out = colspan),
        layout.pos.col = seq(column, length.out = rowspan),
        clip = "on"
      )
    },
    row = .SD$row_index,
    colspan = .SD$colspan,
    column = .SD$col_index,
    rowspan = .SD$rowspan,
    SIMPLIFY = FALSE
  )]

  # drop unneeded columns
  grid_data[, c(
    "col_width", "row_height", "row_min_height", "row_max_height",
    "hrule", "rowspan_group", "colspan_group"
  ) := NULL]

  grid_data
}

grid_data_add_cell_info <- function(grid_data, x) {
  cell_data <- fortify_style(x, "cells")

  # merge with grid_data to keep only active cells
  keycols <- c("part", "ft_row_id", "col_id")
  cell_data <- merge(grid_data[, keycols, with = FALSE], cell_data, by = keycols)

  # generate argument list for creating the cell background rect
  cell_data[, "background" := mapply(
    function(fill) {
      gp <- gpar_background(fill)
      if (length(gp) > 0) {
        list(gp = gp)
      }
    },
    fill = .SD$background.color,
    SIMPLIFY = FALSE
  )]

  # generate argument list for creating the border segments
  cell_data[, "borders" := mapply(
    function(t, l, b, r) {
      list(
        if (length(t) > 0) {
          list(x0 = 0, x1 = 1, y0 = 1, y1 = 1, gp = t)
        },
        if (length(l) > 0) {
          list(x0 = 0, x1 = 0, y0 = 0, y1 = 1, gp = l)
        },
        if (length(b) > 0) {
          list(x0 = 0, x1 = 1, y0 = 0, y1 = 0, gp = b)
        },
        if (length(r) > 0) {
          list(x0 = 1, x1 = 1, y0 = 0, y1 = 1, gp = r)
        }
      )
    },
    t = mapply(
      gpar_border,
      width = cell_data$border.width.top,
      color = cell_data$border.color.top,
      style = cell_data$border.style.top,
      SIMPLIFY = FALSE
    ),
    l = mapply(
      gpar_border,
      width = cell_data$border.width.left,
      color = cell_data$border.color.left,
      style = cell_data$border.style.left,
      SIMPLIFY = FALSE
    ),
    b = mapply(
      gpar_border,
      width = cell_data$border.width.bottom,
      color = cell_data$border.color.bottom,
      style = cell_data$border.style.bottom,
      SIMPLIFY = FALSE
    ),
    r = mapply(
      gpar_border,
      width = cell_data$border.width.right,
      color = cell_data$border.color.right,
      style = cell_data$border.style.right,
      SIMPLIFY = FALSE
    ),
    SIMPLIFY = FALSE
  )]

  # set rotation angle
  cell_data[, "angle" := fcase(
    .SD$text.direction == "tbrl", 270,
    .SD$text.direction == "btlr", 90,
    default = 0
  )]

  # merge needed columns
  cell_data <- cell_data[, c(
    keycols, "background", "borders", "angle", "vertical.align"
  ), with = FALSE]
  grid_data <- merge(grid_data, cell_data, by = keycols, all.x = TRUE)
  setorderv(grid_data, cols = c("row_index", "col_index"))
  grid_data
}

#' @importFrom grid unit
grid_data_add_par_info <- function(grid_data, x) {
  par_data <- fortify_style(x, "pars")

  # merge with grid_data to keep only active cells
  keycols <- c("part", "ft_row_id", "col_id")
  par_data <- merge(grid_data[, c(
    keycols, "vertical.align", "angle"
  ), with = FALSE], par_data, by = keycols)

  # calculate justification
  par_data[, "just" := mapply(
    calc_grid_just,
    halign = .SD$text.align, valign = .SD$vertical.align,
    SIMPLIFY = FALSE
  )]
  # calculate rotated justification
  par_data[, "justr" := mapply(
    calc_grid_rotated_just,
    just = .SD$just, angle = .SD$angle,
    SIMPLIFY = FALSE
  )]

  # set padding as units
  par_data[, "padding.top" := .SD$padding.top / 72]
  par_data[, "padding.left" := .SD$padding.left / 72]
  par_data[, "padding.bottom" := .SD$padding.bottom / 72]
  par_data[, "padding.right" := .SD$padding.right / 72]
  par_data[, "paddingx" := .SD$padding.left + .SD$padding.right]
  par_data[, "paddingy" := .SD$padding.top + .SD$padding.bottom]

  # generate argument list for creating the contents viewport
  par_data[, "contents_vp" := mapply(
    function(just, justr, t, l, b, r, angle) {
      x <- unit(just[[1]], "npc")
      if (just[[1]] == 0) {
        x <- x + unit(l, "in")
      } else if (just[[1]] == 1) {
        x <- x - unit(r, "in")
      }
      y <- unit(just[[2]], "npc")
      if (just[[2]] == 0) {
        y <- y + unit(b, "in")
      } else if (just[[2]] == 1) {
        y <- y - unit(t, "in")
      }
      list(
        x = x,
        y = y,
        just = justr,
        angle = angle,
        width = unit(1, "npc") - unit(l + r, "in"),
        height = unit(1, "npc") - unit(t + b, "in"),
        clip = "inherit"
      )
    },
    just = .SD$just,
    justr = .SD$justr,
    t = .SD$padding.top,
    l = .SD$padding.left,
    b = .SD$padding.bottom,
    r = .SD$padding.right,
    angle = .SD$angle,
    SIMPLIFY = FALSE
  )]

  # merge needed columns
  par_data <- par_data[, c(
    keycols, "line_spacing", "just", "justr", "paddingx", "paddingy", "contents_vp"
  ), with = FALSE]
  grid_data <- merge(grid_data, par_data, by = keycols, all.x = TRUE)
  setorderv(grid_data, cols = c("row_index", "col_index"))

  # merge cell parameters
  grid_data[, "cell_params" := mapply(
    list,
    min_height = .SD$cell_min_height,
    max_height = .SD$cell_max_height,
    just = .SD$just,
    justr = .SD$justr,
    paddingx = .SD$paddingx,
    paddingy = .SD$paddingy,
    angle = .SD$angle,
    lineheight = .SD$line_spacing,
    SIMPLIFY = FALSE
  )]

  grid_data
}

#' @importFrom grDevices is.raster
grid_data_add_chunk_info <- function(grid_data, x, autowidths, wrapping) {
  chunk_data <- fortify_run(x, expand_special_chars = FALSE)

  # merge with grid_data to keep only active cells
  keycols <- c("part", "ft_row_id", "col_id")
  chunk_data <- merge(
    grid_data[, c(keycols, "line_spacing", "angle"), with = FALSE],
    chunk_data,
    by = keycols
  )

  # chunk types
  chunk_data[, "is_raster" :=
    sapply(.SD$img_data, is.raster) | sapply(.SD$img_data, is.character)]
  chunk_data[, "is_equation" := !is.na(.SD$eq_data)]
  chunk_data[, "is_text" := !.SD$is_raster & !.SD$is_equation]

  # avoid CMD check warnings
  is_raster <- is_text <- txt <- is_tab <- is_newline <- is_space <-
    is_superscript <- is_subscript <- angle <- NULL

  # remove empty text
  chunk_data <- chunk_data[!((is_text) & txt %in% ""), , drop = FALSE]

  # remove breaks and tabs from superscript/subscript
  chunk_data[, "is_superscript" := .SD$is_text & .SD$vertical.align %in% "superscript"]
  chunk_data[, "is_subscript" := .SD$is_text & .SD$vertical.align %in% "subscript"]
  chunk_data[(is_superscript | is_subscript), "txt" := gsub("[\n\t]", "", .SD$txt)]

  # set chunk index
  chunk_data[, "chunk_index" := 1:.N, by = keycols]
  setorderv(chunk_data, cols = c(keycols, "chunk_index"))

  # calculate fontface
  chunk_data[(is_text), "font.face" := fcase(
    .SD$bold & !.SD$italic, 2L,
    !.SD$bold & .SD$italic, 3L,
    .SD$bold & .SD$italic, 4L,
    default = 1L
  )]

  # create gpar
  chunk_data[, "gp" := mapply(
    function(is_text, ...) {
      if (is_text) {
        gpar_text(...)
      } else {
        gpar()
      }
    },
    is_text = .SD$is_text,
    color = .SD$color,
    fontfamily = .SD$font.family,
    fontsize = .SD$font.size,
    fontface = .SD$font.face,
    lineheight = .SD$line_spacing,
    SIMPLIFY = FALSE
  )]

  # resolve image data
  chunk_data[(is_raster), "img_data" := mapply(
    calc_grid_image,
    .SD$img_data,
    width = .SD$width,
    height = .SD$height,
    SIMPLIFY = FALSE
  )]

  # create chunk part data (parts split by newline)
  part_data <- chunk_data[, c(
    "part", "ft_row_id", "col_id", "line_spacing", "angle",
    "font.family", "font.size", "italic", "bold",
    "txt", "is_text", "is_superscript", "is_subscript", "width", "height",
    "seq_index", "chunk_index"
  ), with = FALSE]
  # expand by newline
  part_data <- expand_special_char(part_data, what = "\n", with = NA)
  # set part_index per chunk
  setorderv(part_data, cols = c(keycols, "chunk_index", "seq_index"))
  part_data[, "part_index" := 1:.N, by = c(keycols, "chunk_index")]
  setorderv(part_data, cols = c(keycols, "chunk_index", "part_index"))
  # calculate metrics
  part_data <- calc_grid_text_metrics(part_data)

  # calculate now content dimensions if autowidths is TRUE and wrapping is FALSE
  if (autowidths && !wrapping) {
    # create line index per chunk
    part_data[, c("line_index") := fifelse(.SD$is_newline, 1L, 0L)]
    part_data[, c("line_index") := cumsum(.SD$line_index) + 1L, by = keycols]
    # calculate line metrics
    line_data <- part_data[, list(
      line_width = sum(.SD$width, na.rm = TRUE),
      line_height = max(.SD$ascent, na.rm = TRUE) + max(.SD$descent, na.rm = TRUE)
    ), by = c(keycols, "line_index")]
    line_data <- line_data[, list(
      content_width = max(.SD$line_width, na.rm = TRUE),
      content_height = sum(.SD$line_height, na.rm = TRUE)
    ), by = keycols]
    part_data <- merge(part_data, line_data, by = keycols)
    part_data[angle != 0, c("content_width", "content_height") :=
      list(.SD$content_height, .SD$content_width)]

    # merge content_width/content_height to grid data
    content_data <- part_data[, list(
      content_width = first(.SD$content_width),
      content_height = first(.SD$content_height)
    ), by = keycols]
    grid_data <- merge(grid_data, content_data, by = keycols, all.x = TRUE)

    # adjust column widths & row heights
    grid_data <- grid_data_adjust_widths(grid_data)
    grid_data <- grid_data_adjust_heights(grid_data)

    # drop unneeded columns
    part_data[, c("line_index", "content_width", "content_height") := NULL]
    line_data <- NULL
  }

  # handle wrapping
  if (wrapping) {

    # create chunk word data (parts split by whitespace)
    word_data <- part_data
    word_data[, "wrapping" := fifelse(
      .SD$is_text & !.SD$is_tab & !.SD$is_space & !.SD$is_newline &
        !.SD$is_superscript & !.SD$is_subscript, "whitespace", "none"
    )]
    # split by wrapping method
    split_list <- split(word_data, word_data$wrapping, drop = FALSE)
    if (is.data.table(split_list$whitespace)) {
      # expand by whitespace
      split_list$whitespace <- expand_special_char(
        split_list$whitespace,
        what = "[ \t]", with = NA
      )
    }
    # bind them together
    word_data <- rbindlist(split_list, use.names = TRUE, fill = TRUE)
    split_list <- NULL

    # set word_index and word_count per chunk part
    setorderv(word_data, cols = c(keycols, "chunk_index", "part_index", "seq_index"))
    word_data[, c("word_index", "word_count") := list(1:.N, .N),
      by = c(keycols, "chunk_index", "part_index")
    ]
    setorderv(word_data, cols = c(keycols, "chunk_index", "part_index", "word_index"))

    # calculate metrics
    word_data <- calc_grid_text_metrics(word_data)

    # calculate now content dimensions if autowidths is TRUE and wrapping is TRUE
    if (autowidths) {
      # don't take into account whitespace dimensions, except newline char in the beggining
      word_data[, "part_width" := fifelse(
        .SD$is_newline & .SD$part_index > 1 | .SD$is_tab | .SD$is_space, 0, .SD$width
      )]
      word_data[, "part_ascent" := fifelse(
        .SD$is_newline & .SD$part_index > 1 | .SD$is_tab | .SD$is_space, 0, .SD$ascent
      )]
      word_data[, "part_descent" := fifelse(
        .SD$is_newline & .SD$part_index > 1 | .SD$is_tab | .SD$is_space, 0, .SD$descent
      )]

      # calculate content metrics
      word_data[, "content_width" := max(.SD$part_width, na.rm = TRUE), by = keycols]
      word_data[, "content_height" :=
        (sum(.SD$part_width, na.rm = TRUE) / .SD$content_width) *
          (max(.SD$part_ascent, na.rm = TRUE) + max(.SD$part_descent, na.rm = TRUE)), by = keycols]
      word_data[angle != 0, c("content_width", "content_height") :=
        list(.SD$content_height, .SD$content_width)]

      # merge content_width to grid data
      content_data <- word_data[, list(
        content_width = first(.SD$content_width)
      ), by = keycols]
      grid_data <- merge(grid_data, content_data, by = keycols, all.x = TRUE)
      # adjust column widths
      grid_data <- grid_data_adjust_widths(grid_data)
      # get back cell width
      content_data <- grid_data[, list(
        content_width = first(.SD$cell_width)
      ), by = keycols]
      word_data[, "content_width" := NULL]
      word_data <- merge(word_data, content_data, by = keycols, all.x = TRUE)
      # now recalculate the content height
      word_data[, "content_height" :=
        (sum(.SD$part_width, na.rm = TRUE) / .SD$content_width) *
          (max(.SD$part_ascent, na.rm = TRUE) + max(.SD$part_descent, na.rm = TRUE)), by = keycols]
      word_data[angle != 0, c("content_width", "content_height") :=
        list(.SD$content_height, .SD$content_width)]
      # merge content_height to grid data
      content_data <- word_data[, list(
        content_height = first(.SD$content_height)
      ), by = keycols]
      grid_data <- merge(grid_data, content_data, by = keycols, all.x = TRUE)
      # adjust row heights
      grid_data <- grid_data_adjust_heights(grid_data)

      # drop not needed columns
      word_data[, c(
        "content_width", "content_height", "wrapping",
        "part_width", "part_ascent", "part_descent"
      ) := NULL]
    }

    # create chunk word data (parts split by any character)
    char_data <- word_data[
      (is_text & !is_tab & !is_space & !is_newline & !is_superscript & !is_subscript), ,
      drop = FALSE
    ]
    # expand by character
    char_data <- expand_special_char(char_data, what = ".", with = NA)

    # set word_index and word_count per chunk part
    setorderv(char_data, cols = c(keycols, "chunk_index", "part_index", "word_index", "seq_index"))
    char_data[, c("char_index", "char_count") := list(1:.N, .N),
      by = c(keycols, "chunk_index", "part_index", "word_index")
    ]

    # calculate metrics
    char_data <- calc_grid_text_metrics(char_data)

    # merge char_count to word data
    word_char_data <- char_data[, list(
      char_count = first(.SD$char_count)
    ), by = c(keycols, "chunk_index", "part_index", "word_index")]
    word_data <- merge(word_data, word_char_data,
      by = c(keycols, "chunk_index", "part_index", "word_index"), all.x = TRUE
    )

    # merge word_count to part data
    part_word_data <- word_data[, list(
      word_count = first(.SD$word_count),
      char_count = sum(.SD$char_count)
    ), by = c(keycols, "chunk_index", "part_index")]
    part_data <- merge(part_data, part_word_data,
      by = c(keycols, "chunk_index", "part_index"), all.x = TRUE
    )

    # merge part/word/char data together
    word_index <- word_count <- char_index <- char_count <- NULL
    part_data <- rbindlist(
      list(
        part_data,
        word_data[word_count > 1], # only multiple words
        char_data[char_count > 1] # only multiple chars
      ),
      use.names = TRUE, fill = TRUE,
      idcol = "wrap_level" # set idcol to know where data came from
    )
    part_data[is.na(word_index), "word_index" := 0L]
    part_data[is.na(word_count), "word_count" := 0L]
    part_data[is.na(char_index), "char_index" := 0L]
    part_data[is.na(char_count), "char_count" := 0L]
    setorderv(part_data, cols = c(
      keycols, "chunk_index", "part_index", "word_index", "char_index"
    ))
    # set seq_index unique for each cell
    part_data[, "seq_index" := 1:.N, by = keycols]

    # set wrap child count
    part_data[, "child_count" := fcase(
      .SD$wrap_level == 1 & .SD$word_count > 1, .SD$word_count,
      .SD$wrap_level == 1 & .SD$char_count > 1, .SD$char_count,
      .SD$wrap_level == 2, .SD$char_count,
      default = 0L
    )]
    # adjust wrap level
    part_data[, "wrap_level" := fcase(
      .SD$wrap_level == 1, 1L,
      .SD$wrap_level == 2, 2L,
      .SD$wrap_level == 3 & .SD$word_count == 1, 2L,
      default = 3L
    )]

    # calculate wrap children indices
    # for any row which has wrapped children
    part_data[, "children_index" := list(calc_grid_wrap_children(.SD)),
      by = c(keycols, "chunk_index")
    ]
  } else { # no wrapping
    part_data[, "seq_index" := 1:.N, by = keycols]
    part_data[, "child_count" := 0L]
    part_data[, "children_index" := integer(0)]
  }
  setorderv(part_data, cols = c(keycols, "seq_index"))

  # replace special chars
  part_data[(is_newline), "txt" := ""]
  part_data[(is_space), "txt" := " "]
  part_data[(is_tab), "txt" := "    "]

  # merge chunk data
  grid_data <- merge(
    grid_data,
    chunk_data[,
      list(
        chunk_data = list(
          data.table(
            chunk_index = .SD$chunk_index,
            is_text = .SD$is_text,
            is_raster = .SD$is_raster,
            is_equation = .SD$is_equation,
            is_underlined = .SD$underlined,
            is_bold = .SD$bold,
            is_italic = .SD$italic,
            valign = .SD$vertical.align,
            width = .SD$width,
            height = .SD$height,
            gp = .SD$gp,
            txt_data = .SD$txt,
            img_data = .SD$img_data,
            eq_data = .SD$eq_data,
            shading_color = .SD$shading.color
          )
        )
      ),
      by = keycols
    ],
    by = keycols, all.x = TRUE
  )

  # merge part data
  grid_data <- merge(
    grid_data,
    part_data[,
      list(
        chunk_part_data = list(
          data.table(
            seq_index = .SD$seq_index,
            chunk_index = .SD$chunk_index,
            child_count = .SD$child_count,
            children_index = .SD$children_index,
            row_index = 0L,
            col_index = 0L,
            is_newline = .SD$is_newline,
            is_whitespace = .SD$is_tab | .SD$is_space,
            width = .SD$width,
            height = .SD$height,
            ascent = .SD$ascent,
            descent = .SD$descent,
            txt_data = .SD$txt
          )
        )
      ),
      by = keycols
    ],
    by = keycols, all.x = TRUE
  )
  setorderv(grid_data, cols = c("row_index", "col_index"))

  # drop unneeded columns
  grid_data[, c(
    "cell_min_height", "cell_max_height",
    "just", "justr", "paddingx", "paddingy", "angle", "line_spacing", "vertical.align"
  ) := NULL]

  grid_data
}

#' @importFrom data.table fsetdiff
grid_data_adjust_widths <- function(grid_data) {
  # collect the minimum width of each cell
  dat <- grid_data[, list(
    content_min_width = max(
      .SD$cell_width,
      safe_stat(.SD$content_width + .SD$paddingx, FUN = max, NA_value = NA_real_),
      na.rm = TRUE
    )
  ), by = c("col_index", "rowspan")]
  # separate columns with minimum span
  dat_minspan <- dat[, .SD[which.min(.SD$rowspan), ], by = "col_index"]
  # separate columns with maximum span
  dat_maxspan <- fsetdiff(dat, dat_minspan)
  if (nrow(dat_maxspan) > 0) {
    col_index <- NULL
    # recalculate minimum width for spanned cells
    dat_maxspan[, c("prev_width", "content_min_width") := list(
      .SD$content_min_width,
      mapply(
        function(index, span, width) {
          sum(dat_minspan[col_index >= index & col_index < index + span]$content_min_width)
        },
        .SD$col_index, .SD$rowspan, .SD$content_min_width
      )
    )]
    # check the difference of new width vs the previous
    dat_maxspan[, "diff" := .SD$prev_width - .SD$content_min_width]
    # if there is a positive difference, it means that some spanned cells are wider than
    # the cummulative widths of their columns
    # in that case we need to re-adjust the column widths
    if (any(dat_maxspan$diff > 0)) {
      dat_overflow <- dat_maxspan[diff > 0, .SD[which.max(.SD$diff), ], by = "col_index"]
      dat_overflow[, "group" := .SD$col_index]
      dat_overflow <- dat_overflow[, list(
        col_index = seq.int(from = .SD$col_index, length.out = .SD$rowspan, by = 1L),
        diff = .SD$diff / .SD$rowspan
      ), by = "group"]
      dat_overflow <- dat_overflow[, .SD[which.max(.SD$diff), ], by = "col_index"]
      dat_overflow[, "group" := NULL]
      dat <- merge(
        dat,
        dat_overflow,
        by = "col_index",
        all.x = TRUE
      )
      dat[, "diff" := .SD$diff * .SD$rowspan]
      dat[, "content_min_width" := sum(
        .SD$content_min_width, .SD$diff,
        na.rm = TRUE
      ), by = c("col_index", "rowspan")]
      dat[, "diff" := NULL]
      dat_minspan <- dat[, .SD[which.min(.SD$rowspan), ], by = "col_index"]
      dat_maxspan <- fsetdiff(dat, dat_minspan)
      dat_maxspan[, c("prev_width", "content_min_width") := list(
        .SD$content_min_width,
        mapply(
          function(index, span, width) {
            sum(dat_minspan[col_index >= index & col_index < index + span]$content_min_width)
          },
          .SD$col_index, .SD$rowspan, .SD$content_min_width
        )
      )]
    }
    dat <- rbindlist(list(
      dat_minspan[, c("col_index", "rowspan", "content_min_width")],
      dat_maxspan[, c("col_index", "rowspan", "content_min_width")]
    ))
  }
  grid_data <- merge(grid_data, dat, by = c("col_index", "rowspan"), all.x = TRUE)
  grid_data[, "cell_width" := .SD$content_min_width]
  grid_data
}

grid_data_adjust_heights <- function(grid_data) {
  # collect the minimum height of each cell
  dat <- grid_data[, list(
    content_min_height = safe_stat(
      safe_stat(
        .SD$cell_min_height,
        safe_stat(.SD$cell_height, FUN = max, NA_value = NA_real_),
        safe_stat(.SD$content_height + .SD$paddingy, FUN = max, NA_value = NA_real_),
        FUN = max, NA_value = NA_real_
      ),
      .SD$cell_max_height,
      FUN = min, NA_value = NA_real_
    )
  ), by = c("row_index", "colspan")]
  # separate rows with minimum span
  dat_minspan <- dat[, .SD[which.min(.SD$colspan), ], by = "row_index"]
  # separate rows with maximum span
  dat_maxspan <- fsetdiff(dat, dat_minspan)
  if (nrow(dat_maxspan) > 0) {
    row_index <- NULL
    # recalculate minimum height for spanned cells
    dat_maxspan[, c("prev_height", "content_min_height") := list(
      .SD$content_min_height,
      mapply(
        function(index, span, height) {
          sum(dat_minspan[row_index >= index & row_index < index + span]$content_min_height)
        },
        .SD$row_index, .SD$colspan, .SD$content_min_height
      )
    )]
    # check the difference of new height vs the previous
    dat_maxspan[, "diff" := .SD$prev_height - .SD$content_min_height]
    # if there is a positive difference, it means that some spanned cells are taller than
    # the cummulative heights of their rows
    # in that case we need to re-adjust the row heights
    if (any(dat_maxspan$diff > 0)) {
      dat_overflow <- dat_maxspan[diff > 0, .SD[which.max(.SD$diff), ], by = "row_index"]
      dat_overflow[, "group" := .SD$row_index]
      dat_overflow <- dat_overflow[, list(
        row_index = seq.int(from = .SD$row_index, length.out = .SD$colspan, by = 1L),
        diff = diff / .SD$colspan
      ), by = "group"]
      dat_overflow <- dat_overflow[, .SD[which.max(.SD$diff), ], by = "row_index"]
      dat_overflow[, "group" := NULL]
      dat <- merge(
        dat,
        dat_overflow,
        by = "row_index",
        all.x = TRUE
      )
      dat[, "diff" := .SD$diff * .SD$colspan]
      dat[, "content_min_height" := sum(
        .SD$content_min_height, .SD$diff,
        na.rm = TRUE
      ), by = c("row_index", "colspan")]
      dat[, "diff" := NULL]
      dat_minspan <- dat[, .SD[which.min(.SD$colspan), ], by = "row_index"]
      dat_maxspan <- fsetdiff(dat, dat_minspan)
      dat_maxspan[, c("prev_height", "content_min_height") := list(
        .SD$content_min_height,
        mapply(
          function(index, span, height) {
            sum(dat_minspan[row_index >= index & row_index < index + span]$content_min_height)
          },
          .SD$row_index, .SD$colspan, .SD$content_min_height
        )
      )]
    }
    dat <- rbindlist(list(
      dat_minspan[, c("row_index", "colspan", "content_min_height")],
      dat_maxspan[, c("row_index", "colspan", "content_min_height")]
    ))
  }
  grid_data <- merge(grid_data, dat, by = c("row_index", "colspan"), all.x = TRUE)
  grid_data[, "cell_height" := .SD$content_min_height]
  grid_data
}

# gpar --------------------------------------------------------------------

#' @importFrom grid gpar
gpar_background <- function(fill) {
  if (isTRUE(!is.na(fill) && fill != "transparent")) {
    gpar(
      lwd = 0.5,
      fill = fill,
      col = fill
    )
  }
}

gpar_ltys <- c(solid = 1, dashed = 2, dotted = 3, dotdash = 4, longdash = 5, twodash = 6)

gpar_border <- function(width, color, style) {
  if (isTRUE(width > 0 && !is.na(color) && !color %in% "transparent" &&
    (style %in% gpar_ltys || style %in% names(gpar_ltys)))) {
    gpar(
      lwd = width,
      col = color,
      lty = style
    )
  }
}

gpar_text <- function(color, fontfamily, fontsize, fontface, lineheight) {
  gpar(
    col = color,
    fontfamily = fontfamily,
    fontsize = fontsize,
    fontface = fontface,
    lineheight = lineheight
  )
}

# utils -------------------------------------------------------------------

# helper to create grouping by rowspan or colspan
calc_grid_span_group <- function(x) {
  x <- x[x > 0]
  inverse.rle(structure(
    list(lengths = x, values = seq_along(x)),
    class = "rle"
  ))
}

calc_grid_just <- function(halign, valign) {
  c(
    # hjust
    fcase(
      halign == "left", 0,
      halign == "right", 1,
      default = 0.5
    ),
    # vjust
    fcase(
      valign == "bottom", 0,
      valign == "top", 1,
      default = 0.5
    )
  )
}

calc_grid_rotated_just <- function(just, angle) {
  hjust <- just[[1]]
  vjust <- just[[2]]
  c(
    # hjust
    fcase(
      0 <= angle & angle < 90, hjust,
      90 <= angle & angle < 180,
      fifelse(vjust == 0, 0, fifelse(vjust == 1, 1, 1 - vjust)),
      180 <= angle & angle < 270, 1 - hjust,
      270 <= angle & angle < 360,
      fifelse(vjust == 0, 1, fifelse(vjust == 1, 0, vjust))
    ),
    # vjust
    fcase(
      0 <= angle & angle < 90, vjust,
      90 <= angle & angle < 180,
      fifelse(hjust == 0, 1, fifelse(hjust == 1, 0, hjust)),
      180 <= angle & angle < 270, 1 - vjust,
      270 <= angle & angle < 360,
      fifelse(hjust == 0, 0, fifelse(hjust == 1, 1, 1 - hjust))
    )
  )
}

#' @importFrom gdtools str_metrics
calc_grid_text_metrics <- function(dat) {
  # avoid CMD check warnings
  is_text <- is_tab <- is_newline <- is_space <- is_superscript <- is_subscript <- NULL

  # handle special chars
  dat[, "is_newline" := .SD$is_text & .SD$txt %in% "\n"]
  dat[, "is_tab" := .SD$is_text & .SD$txt %in% "\t"]
  dat[, "is_space" := .SD$is_text & .SD$txt %in% " "]
  # temporarily replace these, so that they get some height
  dat[(is_newline), "txt" := "."]
  dat[(is_tab), "txt" := "mmmm"]
  dat[(is_space), "txt" := "."]

  # calculate string metrics
  txt_metrics <- mapply(
    str_metrics,
    dat$txt,
    fontname = dat$font.family,
    fontsize = dat$font.size,
    bold = dat$bold,
    italic = dat$italic,
    USE.NAMES = FALSE
  ) / 72
  txt_metrics <- t(txt_metrics)
  colnames(txt_metrics) <- c("part_width", "part_ascent", "part_descent")
  dat <- cbind(dat, txt_metrics)

  # set width/ascent/descent/height
  dat[(is_text), "width" := .SD$part_width]
  dat[(is_superscript | is_subscript), "width" := .SD$width * 0.75]
  dat[(is_text), "ascent" := .SD$part_ascent * .SD$line_spacing]
  dat[(is_text), "descent" := .SD$part_descent * .SD$line_spacing]
  dat[(is_text), "height" := .SD$ascent + .SD$descent]
  dat[!(is_text), c("ascent", "descent") := list(.SD$height, 0)]
  dat[(is_newline), "txt" := "\n"]
  dat[(is_tab), "txt" := "\t"]
  dat[(is_space), "txt" := " "]

  # drop not needed columns
  dat[, c("part_width", "part_ascent", "part_descent") := NULL]

  # done
  dat
}

calc_grid_wrap_children <- function(dat) {
  wrap_level <- seq_index <- NULL
  mapply(
    function(level, index, child_count) {
      if (child_count > 0) {
        head(
          dat[wrap_level == (level + 1) & seq_index > index, , drop = FALSE],
          n = child_count
        )$seq_index
      } else {
        integer(0)
      }
    },
    dat$wrap_level,
    dat$seq_index,
    dat$child_count,
    SIMPLIFY = FALSE
  )
}

calc_grid_label <- function(x,
                            valign = "baseline",
                            is_underlined = FALSE,
                            is_bold = FALSE,
                            is_italic = FALSE) {
  if (valign %in% c("superscript", "subscript") || isTRUE(is_underlined)) {
    x <- paste0("\"", x, "\"")
    if (valign %in% "superscript") {
      x <- paste0("\"\"^", x)
    } else if (valign %in% "subscript") {
      x <- paste0("\"\"[", x, "]")
    }
    if (isTRUE(is_underlined)) {
      x <- paste0("underline(", x, ")")
    }
    if (isTRUE(is_bold) && isTRUE(is_italic)) {
      x <- paste0("bolditalic(", x, ")")
    } else if (isTRUE(is_bold)) {
      x <- paste0("bold(", x, ")")
    } else if (isTRUE(is_italic)) {
      x <- paste0("italic(", x, ")")
    }
    parse(text = x)
  } else {
    x
  }
}

blank_raster_image <- as.raster(matrix("transparent", nrow = 1, ncol = 1))

calc_grid_image <- function(img_data, width = NULL, height = NULL) {
  image <- blank_raster_image
  if (is.raster(img_data)) {
    image <- img_data
    class(image) <- c("flextableRasterChunk", class(image))
  } else if (is.character(img_data)) {
    if (!file.exists(img_data)) {
      warning(sprintf("file '%s' can not be found.", img_data))
    } else if (requireNamespace("magick", quietly = TRUE)) {
      tryCatch(
        {
          if (grepl("\\.svg$", ignore.case = TRUE, x = img_data)) {
            image <- magick::image_read_svg(img_data, width * 72, height * 72)
          } else {
            image <- magick::image_read(img_data)
          }
          image <- as.raster(image)
        },
        error = function(e) {
          warning("error reading image: ", e)
        }
      )
    } else {
      warning(sprintf("package 'magick' is required to read image files"))
    }
  }
  image
}
