///|
/// Decode a P/Q descriptor after checking that its referenced bytes are wholly
/// inside the declared heap. A zero-length array has no referenced storage.
fn decode_heap_cell(
  data : Bytes,
  table : BinaryTable,
  row_index : Int,
  column : TableColumn,
  descriptor_start : Int,
  element : TableColumnKind,
) -> Result[TableValue, FitsError] {
  if column.repeat == 0 {
    return decode_heap_payload(data, 0, 0, column, element)
  }
  let (count64, offset64) = match column.kind {
    Variable32(_) =>
      (
        read_int32_be(data, descriptor_start),
        read_int32_be(data, descriptor_start + 4),
      )
    Variable64(_) =>
      (
        read_int64_be(data, descriptor_start),
        read_int64_be(data, descriptor_start + 8),
      )
    _ => return Err(InvalidTableCell(column.index, "expected P/Q descriptor"))
  }
  if count64 < 0L || count64 > 2147483647L {
    return Err(
      InvalidHeapDescriptor(
        column.index,
        row_index,
        "element count is negative or too large",
      ),
    )
  }
  let count = count64.to_int()
  match column.max_elements {
    Some(maximum) if count > maximum =>
      return Err(
        InvalidHeapDescriptor(
          column.index,
          row_index,
          "element count \{count} exceeds TFORM maximum \{maximum}",
        ),
      )
    _ => ()
  }
  if count == 0 {
    return decode_heap_payload(data, 0, 0, column, element)
  }
  if offset64 < 0L || offset64 > 2147483647L {
    return Err(
      InvalidHeapDescriptor(
        column.index,
        row_index,
        "heap offset is negative or too large",
      ),
    )
  }
  let offset = offset64.to_int()
  let byte_length = match table_column_width(element, count) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if offset > table.heap_length || byte_length > table.heap_length - offset {
    return Err(
      InvalidHeapDescriptor(
        column.index,
        row_index,
        "array extends beyond declared heap",
      ),
    )
  }
  let start = match
    checked_sum(table.heap_offset, offset, "heap array offset") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  decode_heap_payload(data, start, count, column, element)
}

///|
/// Reuse the fixed-cell decoder so heap and inline values have identical
/// treatment of endianness, logical nulls, ASCII and packed bits.
fn decode_heap_payload(
  data : Bytes,
  start : Int,
  count : Int,
  column : TableColumn,
  element : TableColumnKind,
) -> Result[TableValue, FitsError] {
  let width = match table_column_width(element, count) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let end = match checked_sum(start, width, "heap array end") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if start < 0 || start > data.length() || end > data.length() {
    return Err(
      Truncated(
        start,
        width,
        if start <= data.length() {
          data.length() - start
        } else {
          0
        },
      ),
    )
  }
  decode_fixed_cell(data, start, element, count, column.index)
}

///|
fn heap_cell_count(cell : TableValue) -> Int {
  match cell {
    Text(value) => value.to_array().length()
    Logicals(values) => values.length()
    Bits(values) => values.length()
    Integers(values) => values.length()
    Reals(values) => values.length()
  }
}

///|
fn encode_heap_cell(
  row_output : Array[Byte],
  heap_output : Array[Byte],
  column : TableColumn,
  row_index : Int,
  element : TableColumnKind,
  cell : TableValue,
  heap_limit : Int,
) -> Result[Unit, FitsError] {
  let count = heap_cell_count(cell)
  match column.max_elements {
    Some(maximum) if count > maximum =>
      return Err(
        InvalidHeapDescriptor(
          column.index,
          row_index,
          "element count \{count} exceeds TFORM maximum \{maximum}",
        ),
      )
    _ => ()
  }
  if column.repeat == 0 && count != 0 {
    return Err(
      InvalidHeapDescriptor(
        column.index,
        row_index,
        "zero-repeat descriptor requires an empty cell",
      ),
    )
  }
  let width = match table_column_width(element, count) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if width > heap_limit - heap_output.length() {
    return Err(
      InvalidHeapDescriptor(
        column.index,
        row_index,
        "encoded array exceeds declared heap length",
      ),
    )
  }
  let offset = heap_output.length()
  let payload_column = {
    index: column.index,
    name: column.name,
    format: column.format,
    repeat: count,
    offset: 0,
    width,
    kind: element,
    max_elements: None,
  }
  match encode_fixed_cell(heap_output, payload_column, cell) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if column.repeat == 1 {
    let descriptor_width = match column.kind {
      Variable32(_) => 4
      Variable64(_) => 8
      _ => return Err(InvalidTableCell(column.index, "expected P/Q descriptor"))
    }
    append_integer_be(row_output, count.to_int64(), descriptor_width)
    append_integer_be(row_output, offset.to_int64(), descriptor_width)
  }
  Ok(())
}

///|
/// Encode all rows, an optional THEAP gap, and the heap into one BINTABLE data
/// unit. The supplied table's PCOUNT/THEAP geometry is authoritative: heap
/// payloads are packed in row-major order and unused bytes are zero-filled.
/// This function does not write FITS header cards or 2880-byte HDU padding.
pub fn encode_binary_table_data(
  table : BinaryTable,
  rows : Array[Array[TableValue]],
) -> Result[Bytes, FitsError] {
  match validate_table_columns(table) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if rows.length() != table.row_count {
    return Err(InvalidTableRowCount(table.row_count, rows.length()))
  }
  let row_output = Array(capacity=table.row_length * table.row_count)
  let heap_output : Array[Byte] = []
  for row_index, row in rows {
    if row.length() != table.columns.length() {
      return Err(InvalidTableCellCount(table.columns.length(), row.length()))
    }
    for column_index, column in table.columns {
      let result = match column.kind {
        Variable32(element) | Variable64(element) =>
          encode_heap_cell(
            row_output,
            heap_output,
            column,
            row_index,
            element,
            row[column_index],
            table.heap_length,
          )
        _ => encode_fixed_cell(row_output, column, row[column_index])
      }
      match result {
        Err(error) => return Err(error)
        Ok(_) => ()
      }
    }
  }
  let row_bytes = match
    checked_product(table.row_length, table.row_count, "binary-table row area") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let heap_start = table.heap_offset - table.data_offset
  let total_length = match
    checked_sum(heap_start, table.heap_length, "binary-table data length") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let output = Array(capacity=total_length)
  for byte in row_output {
    output.push(byte)
  }
  for index = row_bytes; index < heap_start; index = index + 1 {
    output.push(b'\x00')
  }
  for byte in heap_output {
    output.push(byte)
  }
  for index = heap_output.length(); index < table.heap_length; index = index + 1 {
    output.push(b'\x00')
  }
  Ok(Bytes::from_array(output))
}