// Row-order and row-numbering verbs: `reverse` flips the row order,
// `with_row_index` prepends a positional counter. Neither can fail on the
// *cells* — one is a permutation that is in bounds by construction, the other a
// column append over an already-validated frame. That makes `reverse` total;
// `with_row_index` still raises on what a new column can clash with: a name the
// frame already carries, and a counter that would run past `Int64::MAX`.

///|
/// Reverse the row order, keeping every column and its dtype — Polars'
/// `df.reverse()`. The schema is untouched, so a reversed frame is a frame
/// with the same columns in the same order, read bottom-up.
///
/// Total: the permutation is `[n-1, …, 0]`, in bounds by construction. A
/// 0-row or 1-row frame is returned unchanged rather than gathered into an
/// identical copy.
pub fn DataFrame::reverse(self : DataFrame) -> DataFrame {
  let n = self.nrows()
  if n <= 1 {
    return self
  }
  // Every index comes from `[0, n)`, so the gather cannot fail.
  let perm = Array::makei(n, i => n - 1 - i)
  let columns = self.column_series().map(c => gather_series(c, perm))
  self.with_same_schema(columns, n)
}

///|
/// Prepend a row-number column — Polars' `df.with_row_index(name, offset)`.
/// The counter is an `Int` column named `name` (default `"index"`) holding
/// `offset`, `offset + 1`, … in row order, and it lands **first**, ahead of
/// the frame's own columns, as in Polars.
///
/// Raises `DuplicateColumn(name)` when the frame already has a column of that
/// name — the frame's own invariant, surfaced by the `Schema` the rebuild
/// derives (`DataFrame::from_parts_with_fields`). The
/// counter is dense and always non-null, so it never changes any other
/// column's dtype or nullability.
///
/// Raises `InvalidOperation` when the last number would not fit: the counter
/// runs `offset ..= offset + nrows - 1`, and past `Int64::MAX` MoonBit's
/// wrapping addition would carry it round to `Int64::MIN`, leaving a column
/// that is neither increasing nor dense. Polars refuses the same overflow
/// against its own index type.
pub fn DataFrame::with_row_index(
  self : DataFrame,
  name? : String = "index",
  offset? : Int64 = 0,
) -> DataFrame raise @types.DataError {
  let n = self.nrows()
  // Rearranged to `offset > MAX - (n - 1)` so the check itself cannot overflow:
  // `n - 1` is at most `Int::MAX`, far inside the subtraction's range. Only the
  // top end can run out — every counter step is upward, so a negative `offset`
  // moves away from `Int64::MIN`, never toward it.
  if n > 0 && offset > 9223372036854775807L - (n - 1).to_int64() {
    raise @types.DataError::InvalidOperation(
      "row index overflows Int64: offset \{offset} with \{n} rows",
    )
  }
  let index = @series.Series::from_ints(
    name,
    Array::makei(n, i => offset + i.to_int64()),
  )
  let columns = [index]
  for column in self.column_series() {
    columns.push(column)
  }
  // Prepending a column leaves every existing one exactly where it was, so
  // each keeps the field it arrived with rather than a re-derived one.
  let fields = [@types.Field::Field(name, @types.DataType::Int)]
  for field in self.schema.fields() {
    fields.push(field)
  }
  DataFrame::from_parts_with_fields(columns, fields, n)
}