# internal function
# style_tt() stores style calls and we only want to evaluate them at the end because
# some rows may be added, which changes how the style is applied
#
# THE ORDER MATTERS A LOT!

#' Build group header and body parts with position calculations
#' @keywords internal
#' @noRd
rbind_body_groupi <- function(x) {
  # Reconstruct the final table by combining formatted data_body and group_data_i parts
  if (nrow(x@group_data_i) == 0) {
    # No groups - @data_body already contains the final formatted data
    return(x)
  }

  # Calculate total final table size
  total_rows <- nrow(x@data_body) + nrow(x@group_data_i)
  final_ncol <- ncol(x@data_body)

  # Create final data frame with proper structure
  final_df <- data.frame(matrix(
    NA_character_,
    nrow = total_rows,
    ncol = final_ncol
  ))
  colnames(final_df) <- colnames(x@data_body)

  # Insert body data at index_body positions
  if (nrow(x@data_body) > 0 && length(x@index_body) > 0) {
    ok <- !is.na(x@index_body) & x@index_body >= 1 & x@index_body <= total_rows
    if (any(ok)) {
      final_df[x@index_body[ok], ] <- x@data_body[which(ok), , drop = FALSE]
    }
  }

  # Insert group i data at group_index_i positions
  if (nrow(x@group_data_i) > 0 && length(x@group_index_i) > 0) {
    idx <- x@group_index_i[seq_len(nrow(x@group_data_i))]
    ok <- !is.na(idx) & idx >= 1 & idx <= total_rows
    if (any(ok)) {
      final_df[idx[ok], ] <- x@group_data_i[which(ok), , drop = FALSE]
    }
  }

  x@data_body <- final_df

  # before format_tt() because we need the indices
  x@nrow <- nrow(x@data) + nrow(x@group_data_i)

  return(x)
}


build_tt <- function(x, output = NULL) {
  x <- sanitize_output(x, output)
  output <- infer_output(x)

  x <- switch(output,
    html = {
      if (identical(x@html_engine, "tabulator")) {
        swap_class(x, "tinytable_tabulator")
      } else {
        swap_class(x, "tinytable_html")
      }
    },
    latex = swap_class(x, "tinytable_tabularray"),
    markdown = swap_class(x, "tinytable_grid"),
    typst = swap_class(x, "tinytable_typst"),
    dataframe = swap_class(x, "tinytable_dataframe"),
    stop("Unsupported output format: '", output, "'. Supported formats are: html, latex, markdown, typst, dataframe", call. = FALSE)
  )

  x@output <- output


  # Calculate which positions are body vs group
  if (nrow(x@group_data_i) == 0) {
    x@index_body <- seq_len(nrow(x))
  } else {
    x@index_body <- setdiff(seq_len(nrow(x)), x@group_index_i)
  }

  # format each component individually, including groups before inserting them into the body
  for (l in x@lazy_format) {
    o <- attr(l, "output")
    if (is.null(o) || x@output %in% o) {
      l[["x"]] <- x
      x <- eval(l)
    }
  }

  # insert group rows into body
  x <- rbind_body_groupi(x)

  # pre-process: theme_*() calls that need formatting conditional on @output
  # this is useful after rbind() because we now have the final indices and headers
  for (p in x@lazy_prepare) {
    o <- attr(p, "output")
    if (is.null(o) || x@output %in% o) {
      x <- p(x)
    }
  }

  # plots and images
  for (l in x@lazy_plot) {
    o <- attr(l, "output")
    if (is.null(o) || x@output %in% o) {
      l[["x"]] <- x
      x <- eval(l)
    }
  }

  # add footnote markers just after formatting, otherwise appending converts to string
  x <- footnote_markers(x)

  # Create rectangular style dataframe with one row per cell
  # This happens after row group operations so we have the final row structure
  iseq <- seq_len(nrow(x))
  # include headers: 0, -1, ... for each header row, but only when there are
  # header rows. The old expression `-1 * 0:(x@nhead - 1)` produced c(0, 1)
  # when nhead == 0, adding a phantom header row and duplicating body row 1.
  if (x@nhead > 0) {
    iseq <- c(-(seq_len(x@nhead) - 1L), iseq)
  }
  jseq <- seq_len(ncol(x))
  rect <- expand.grid(i = iseq, j = jseq)

  # Initialize all style columns with NA
  for (col in STYLE_PROPS) {
    rect[[col]] <- NA
  }

  x@style_other <- rect

  # apply style_tt() and theme_*() after all group operations.
  # style_tt_lazy() exposes only the current call's settings frame in @style;
  # collect those frames here and rbind() them once. This replaces the previous
  # pattern where every style_tt() call grew @style with an incremental rbind(),
  # an O(N^2) cost that dominated on tables with many style_tt() calls.
  lazy_style <- x@lazy_style
  x@lazy_style <- list()

  style_frames <- vector("list", length(lazy_style))
  for (k in seq_along(lazy_style)) {
    p <- lazy_style[[k]]
    o <- attr(p, "output")
    if (is.null(o) || x@output %in% o) {
      p[["x"]] <- x
      x <- eval(p)
      if (nrow(x@style) > 0L) style_frames[[k]] <- x@style
    }
  }
  style_frames <- style_frames[!vapply(style_frames, is.null, logical(1))]
  x@style <- if (length(style_frames)) do.call(rbind, style_frames) else data.frame()

  # Fix colspan that exceeds column count after lazy styles are evaluated
  x@style <- clamp_colspan(x@style, x@ncol)

  # apply styling AFTER formatting/escaping to avoid escaping the style brackets
  x <- style_notes(x)
  x <- style_caption(x)

  # Populate @style_other and @style_lines by resolving @style in a single
  # batched pass. This must happen before build_eval() for backends (like
  # Grid) that apply styles during build.
  # The original per-row loop with apply_style_to_rect()/append_lines_to_rect()
  # is preserved here as a fallback; for non-trivial style counts the batched
  # resolver in resolve_styles_batch() is dramatically faster because it does
  # not rescan the entire rectangular grid for every style_tt() entry.
  resolved <- resolve_styles_batch(x@style_other, x@style)
  has_style <- resolved$has_style
  x@style_other <- resolved$other
  x@style_lines <- resolved$lines

  # Sort style_other for consistent ordering (j first, then i within each j)
  # This ensures test snapshots are deterministic
  ord <- order(x@style_other$j, x@style_other$i)
  x@style_other <- x@style_other[ord, ]
  attr(x@style_other, "has_style") <- has_style[ord]

  # inline plots must be re-boxed for `alignv` to have an effect in LaTeX (#673)
  x <- plot_alignv_latex(x)

  # draw the table
  x <- build_eval(x)

  # groups require the table to be drawn first, especially group_tabularray_col() and friends
  # Handle column groups from @group_data_j
  if (nrow(x@group_data_j) > 0) {
    # Calculate ihead for the group headers - start from -1 for the top header row
    ihead <- -1
    # Apply group_eval_j once with all groups
    x <- group_eval_j(x, j = seq_len(ncol(x)), ihead = ihead)
  }

  x <- style_eval(x)

  # no-op: only modifies markdown
  x <- grid_colspan(x)

  x <- finalize(x)

  # post-process: theme_*() calls that need formatting conditional on @output and table drawn
  for (p in x@lazy_finalize) {
    o <- attr(p, "output")
    if (is.null(o) || x@output %in% o) {
      x <- p(x)
    }
  }

  x@table_string <- lines_drop_consecutive_empty(x@table_string)
  if (x@markdown_style == "gfm") {
    assert_dependency("pandoc")
    x@table_string <- paste(
      pandoc::pandoc_convert(text = x@table_string, to = "gfm"),
      collapse = "\n"
    )
  }

  return(x)
}


build_prepare <- function(x, fn, output = NULL) {
  attr(fn, "output") <- output
  x@lazy_prepare <- c(x@lazy_prepare, list(fn))
  return(x)
}


build_finalize <- function(x, fn, output = NULL) {
  attr(fn, "output") <- output
  x@lazy_finalize <- c(x@lazy_finalize, list(fn))
  return(x)
}
