///|
fn push_value_card(
  cards : Array[Card],
  keyword : String,
  value : String,
) -> Unit {
  cards.push({
    keyword,
    value: Some(value),
    comment: None,
    record_index: cards.length(),
  })
}

///|
fn quoted_table_text(value : String) -> String {
  let output = StringBuilder()
  output.write_char('\'')
  for ch in value {
    output.write_char(ch)
    if ch == '\'' {
      output.write_char('\'')
    }
  }
  output.write_char('\'')
  output.to_string()
}

///|
fn push_table_fields(cards : Array[Card], fields : Array[TableField]) -> Unit {
  for index, field in fields {
    let number = index + 1
    match field.name {
      None => ()
      Some(name) =>
        push_value_card(cards, "TTYPE\{number}", quoted_table_text(name))
    }
    push_value_card(cards, "TFORM\{number}", quoted_table_text(field.format))
  }
}

///|
fn build_table_header(
  fields : Array[TableField],
  row_length : Int,
  row_count : Int,
  heap_length : Int,
) -> Header {
  let cards : Array[Card] = []
  push_value_card(cards, "XTENSION", "'BINTABLE'")
  push_value_card(cards, "BITPIX", "8")
  push_value_card(cards, "NAXIS", "2")
  push_value_card(cards, "NAXIS1", "\{row_length}")
  push_value_card(cards, "NAXIS2", "\{row_count}")
  push_value_card(cards, "PCOUNT", "\{heap_length}")
  push_value_card(cards, "GCOUNT", "1")
  push_value_card(cards, "TFIELDS", "\{fields.length()}")
  push_table_fields(cards, fields)
  cards.push({
    keyword: "END",
    value: None,
    comment: None,
    record_index: cards.length(),
  })
  { cards, data_offset: 0, }
}

///|
/// Construct a complete BINTABLE extension from column formats and typed rows.
/// Row width, row count, PCOUNT and P/Q heap offsets are derived automatically;
/// the heap is packed without a THEAP gap. The result includes a canonical
/// header and FITS block padding, ready to append after a primary HDU.
///
/// # Example
/// ```mbt check
/// test {
///   let extension = @moonastrofits.encode_binary_table_extension(
///     [{ name: Some("FLUX"), format: "1E", }],
///     [[@moonastrofits.Reals([1.5])]],
///   ).unwrap()
///   assert_eq(extension.length(), 5760)
/// }
/// ```
pub fn encode_binary_table_extension(
  fields : Array[TableField],
  rows : Array[Array[TableValue]],
) -> Result[Bytes, FitsError] {
  if fields.length() > 999 {
    return Err(InvalidParameter("TFIELDS", fields.length()))
  }
  let schema = build_table_header(fields, 0, rows.length(), 0)
  let columns : Array[TableColumn] = []
  let mut row_length = 0
  for index = 0; index < fields.length(); index = index + 1 {
    let column = match parse_table_column(schema, index + 1, row_length) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    row_length = match
      checked_sum(row_length, column.width, "binary-table row width") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    columns.push(column)
  }
  let row_bytes = match
    checked_product(row_length, rows.length(), "binary-table row area") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let mut heap_length = 0
  for row_index, row in rows {
    if row.length() != columns.length() {
      return Err(InvalidTableCellCount(columns.length(), row.length()))
    }
    for index, column in columns {
      match column.kind {
        Variable32(element) | Variable64(element) => {
          let count = heap_cell_count(row[index])
          if column.repeat == 0 && count != 0 {
            return Err(
              InvalidHeapDescriptor(
                column.index,
                row_index,
                "zero-repeat descriptor requires an empty cell",
              ),
            )
          }
          match column.max_elements {
            Some(maximum) if count > maximum =>
              return Err(
                InvalidHeapDescriptor(
                  column.index,
                  row_index,
                  "element count \{count} exceeds TFORM maximum \{maximum}",
                ),
              )
            _ => ()
          }
          let width = match table_column_width(element, count) {
            Err(error) => return Err(error)
            Ok(value) => value
          }
          heap_length = match
            checked_sum(heap_length, width, "binary-table heap length") {
            Err(error) => return Err(error)
            Ok(value) => value
          }
        }
        _ => ()
      }
    }
  }
  let table : BinaryTable = {
    columns,
    row_length,
    row_count: rows.length(),
    data_offset: 0,
    heap_offset: row_bytes,
    heap_length,
  }
  let data = match encode_binary_table_data(table, rows) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let header = build_table_header(
    fields,
    row_length,
    rows.length(),
    heap_length,
  )
  encode_extension_hdu(header, data)
}