// Shared table-rendering primitives for `DataFrame::to_html` and
// `DataFrame::to_markdown`. Both materialise the same visible window of cells
// and report the same truncation count; only the surrounding syntax (an HTML
// `` vs a GFM pipe table) and the per-cell escaping differ, so the parts
// that don't differ live here once rather than once per renderer.

///|
/// The visible-row count both renderers clamp to: no cap renders every row,
/// a cap saturates into `[0, nrows]` (`clamp_take`, the same limit behind
/// `head` / `tail`). The row-cap rule stated once, so a renderer cannot
/// decide its own truncation semantics.
fn visible_rows(nrows : Int, max_rows : Int?) -> Int {
  match max_rows {
    None => nrows
    Some(limit) => @order.clamp_take(limit, nrows)
  }
}

///|
/// Materialise the first `shown` rows of `df` as a row-major matrix of cell
/// strings — `result[r][c]` is column `c`'s value in row `r`, rendered with the
/// value-form of `Scalar::to_string` (a null cell is the empty string). Slices
/// each column to its first `shown` cells and scalarises only those,
/// then transposes the window into per-row cells — so the work scales with
/// `shown`, not the frame height (a `to_markdown(df, max_rows=10)` over a
/// million-row frame touches ten rows, not a million). The strings are
/// **unescaped**: each renderer applies its own escaping (HTML entities vs GFM
/// pipe-escaping) on top. `shown <= df.nrows()` and every column has
/// `df.nrows()` cells, so each slice is in-bounds. Total.
fn DataFrame::table_cell_matrix(
  self : DataFrame,
  shown : Int,
) -> Array[Array[String]] {
  let col_scalars = self
    .column_series()
    .map(s => s.slice_series(0, shown).to_scalars())
  let ncols = self.ncols()
  Array::makei(shown, r => {
    Array::makei(ncols, c => col_scalars[c][r].to_string())
  })
}

///|
/// The truncation banner text both renderers show when a row cap hides `hidden`
/// rows: `... (N more rows)`. `to_markdown` appends it as a trailing line and
/// `to_html` wraps it in a `` cell, but the wording lives here so the two
/// never drift.
fn more_rows_banner(hidden : Int) -> String {
  if hidden == 1 {
    "... (1 more row)"
  } else {
    "... (\{hidden} more rows)"
  }
}