///|
/// Return the number of data rows in a header-aware table.
pub fn table_row_count(table : CsvTable) -> Int {
  table.rows.length()
}

///|
/// Return the number of columns declared by the table header.
pub fn table_column_count(table : CsvTable) -> Int {
  table.headers.length()
}

///|
/// Keep only the requested columns, in the order requested.
pub fn table_select_columns(
  table : CsvTable,
  columns : Array[String],
) -> CsvTable {
  let indices : Array[Int] = Array::new()
  let headers : Array[String] = Array::new()
  for column in columns {
    match table_ops_column_index(table.headers, column) {
      Some(index) => {
        indices.push(index)
        headers.push(column)
      }
      None => ()
    }
  }
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let next : Array[String] = Array::new()
    for index in indices {
      next.push(table_ops_cell_at(row, index))
    }
    rows.push(next)
  }
  { headers, rows }
}

///|
/// Drop the requested columns while preserving all other columns in their original order.
pub fn table_drop_columns(
  table : CsvTable,
  columns : Array[String],
) -> CsvTable {
  let keep : Array[String] = Array::new()
  for header in table.headers {
    if !table_ops_contains(columns, header) {
      keep.push(header)
    }
  }
  table_select_columns(table, keep)
}

///|
/// Rename a column. If the column is absent, the table is returned unchanged.
pub fn table_rename_column(
  table : CsvTable,
  old_name : String,
  new_name : String,
) -> CsvTable {
  let headers : Array[String] = Array::new()
  let mut changed = false
  for header in table.headers {
    if header == old_name {
      headers.push(new_name)
      changed = true
    } else {
      headers.push(header)
    }
  }
  if changed {
    { headers, rows: table.rows }
  } else {
    table
  }
}

///|
/// Filter rows whose column value equals the provided value.
pub fn table_filter_eq(
  table : CsvTable,
  column : String,
  value : String,
) -> CsvTable {
  match table_ops_column_index(table.headers, column) {
    Some(index) => {
      let rows : Array[Array[String]] = Array::new()
      for row in table.rows {
        if table_ops_cell_at(row, index) == value {
          rows.push(row)
        }
      }
      { headers: table.headers, rows }
    }
    None => { headers: table.headers, rows: [] }
  }
}

///|
/// Filter rows for which the target column is present and not empty.
pub fn table_filter_not_empty(table : CsvTable, column : String) -> CsvTable {
  match table_ops_column_index(table.headers, column) {
    Some(index) => {
      let rows : Array[Array[String]] = Array::new()
      for row in table.rows {
        if !table_ops_cell_at(row, index).is_empty() {
          rows.push(row)
        }
      }
      { headers: table.headers, rows }
    }
    None => { headers: table.headers, rows: [] }
  }
}

///|
/// Sort rows lexicographically by a column, with missing cells treated as empty strings.
pub fn table_sort_by_column(table : CsvTable, column : String) -> CsvTable {
  match table_ops_column_index(table.headers, column) {
    Some(index) => {
      let rows = table.rows.copy()
      for i in 1.. 0 &&
              current_key.lexical_compare(table_ops_cell_at(rows[j - 1], index)) <
              0 {
          rows[j] = rows[j - 1]
          j -= 1
        }
        rows[j] = current
      }
      { headers: table.headers, rows }
    }
    None => { headers: table.headers, rows: table.rows }
  }
}

///|
/// Return at most the first `count` rows.
pub fn table_limit(table : CsvTable, count : Int) -> CsvTable {
  if count <= 0 {
    { headers: table.headers, rows: [] }
  } else {
    table_slice_rows(table, 0, count)
  }
}

///|
/// Return rows in the half-open range `[start, end)`.
pub fn table_slice_rows(table : CsvTable, start : Int, end : Int) -> CsvTable {
  let from = if start < 0 { 0 } else { start }
  let to = if end > table.rows.length() { table.rows.length() } else { end }
  if from >= to {
    { headers: table.headers, rows: [] }
  } else {
    let rows : Array[Array[String]] = Array::new()
    for i in from.. CsvTable {
  if count <= 0 {
    table
  } else {
    table_slice_rows(table, count, table.rows.length())
  }
}

///|
/// Replace empty or missing cells with a default value and pad all rows to header width.
pub fn table_fill_empty(table : CsvTable, default_value : String) -> CsvTable {
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let next : Array[String] = Array::new()
    for i in 0.. CsvTable {
  let headers : Array[String] = Array::new()
  for header in table.headers {
    headers.push(table_ops_trim_ascii(header))
  }
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let next : Array[String] = Array::new()
    for field in row {
      next.push(table_ops_trim_ascii(field))
    }
    rows.push(next)
  }
  { headers, rows }
}

///|
/// Normalize headers by trimming whitespace, lowercasing, and replacing spaces with underscores.
pub fn table_normalize_headers(table : CsvTable) -> CsvTable {
  let headers : Array[String] = Array::new()
  for header in table.headers {
    headers.push(table_ops_slug_header(header))
  }
  { headers, rows: table.rows }
}

///|
/// Remove duplicate rows while preserving the first occurrence.
pub fn table_deduplicate(table : CsvTable) -> CsvTable {
  let keys : Array[String] = Array::new()
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let key = table_ops_row_key(row)
    if !table_ops_contains(keys, key) {
      keys.push(key)
      rows.push(row)
    }
  }
  { headers: table.headers, rows }
}

///|
/// Add a computed column. The callback receives each original row.
pub fn table_add_column(
  table : CsvTable,
  column : String,
  compute : (Array[String]) -> String,
) -> CsvTable {
  let headers = table.headers.copy()
  headers.push(column)
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let next = row.copy()
    next.push(compute(row))
    rows.push(next)
  }
  { headers, rows }
}

///|
/// Convert a table back into raw rows, with headers as the first row.
pub fn table_to_rows(table : CsvTable) -> Array[Array[String]] {
  let rows : Array[Array[String]] = Array::new()
  rows.push(table.headers)
  for row in table.rows {
    rows.push(row)
  }
  rows
}

///|
fn table_ops_column_index(headers : Array[String], column : String) -> Int? {
  for i in 0.. String {
  if index >= 0 && index < row.length() {
    row[index]
  } else {
    ""
  }
}

///|
fn table_ops_contains(values : Array[String], value : String) -> Bool {
  for item in values {
    if item == value {
      return true
    }
  }
  false
}

///|
fn table_ops_trim_ascii(value : String) -> String {
  let chars : Array[Char] = Array::new()
  for ch in value.iter() {
    chars.push(ch)
  }
  let mut start = 0
  let mut finish = chars.length()
  while start < finish && table_ops_is_ascii_space(chars[start]) {
    start += 1
  }
  while finish > start && table_ops_is_ascii_space(chars[finish - 1]) {
    finish -= 1
  }
  let out = StringBuilder()
  for i in start.. String {
  let trimmed = table_ops_trim_ascii(value).to_lower()
  let out = StringBuilder()
  let mut last_was_sep = false
  for ch in trimmed.iter() {
    if ch == ' ' || ch == '-' || ch == '\t' || ch == '\n' || ch == '\r' {
      if !last_was_sep {
        out.write_char('_')
        last_was_sep = true
      }
    } else {
      out.write_char(ch)
      last_was_sep = false
    }
  }
  out.to_string()
}

///|
fn table_ops_is_ascii_space(ch : Char) -> Bool {
  ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}

///|
fn table_ops_row_key(row : Array[String]) -> String {
  let out = StringBuilder()
  for i in 0.. 0 {
      out.write_char('\u{1f}')
    }
    out.write_string(row[i])
  }
  out.to_string()
}