///|
fn required_table_integer(
  header : Header,
  keyword : String,
) -> Result[Int, FitsError] {
  match header.get(keyword) {
    None => Err(MissingKeyword(keyword))
    Some(card) => parse_card_integer(card)
  }
}

///|
fn table_column_width(
  kind : TableColumnKind,
  repeat : Int,
) -> Result[Int, FitsError] {
  match kind {
    Bit => Ok(repeat / 8 + (if repeat % 8 == 0 { 0 } else { 1 }))
    Variable32(_) => checked_product(repeat, 8, "P descriptor width")
    Variable64(_) => checked_product(repeat, 16, "Q descriptor width")
    _ => {
      let unit_width = match kind {
        Ascii | Logical | UnsignedByte => 1
        Int16 => 2
        Int32 | Float32 => 4
        Int64 | Float64 => 8
        Bit => abort("bit width handled above")
        Variable32(_) | Variable64(_) => abort("descriptor width handled above")
      }
      checked_product(repeat, unit_width, "binary-table column width")
    }
  }
}

///|
fn validate_table_columns(table : BinaryTable) -> Result[Unit, FitsError] {
  if table.row_length < 0 {
    return Err(InvalidParameter("NAXIS1", table.row_length))
  }
  if table.row_count < 0 {
    return Err(InvalidParameter("NAXIS2", table.row_count))
  }
  if table.data_offset < 0 {
    return Err(InvalidTableCell(0, "negative table data offset"))
  }
  let mut offset = 0
  for column in table.columns {
    if column.repeat < 0 {
      return Err(InvalidTableCell(column.index, "repeat must be non-negative"))
    }
    match column.kind {
      Variable32(element) | Variable64(element) => {
        if column.repeat > 1 {
          return Err(
            InvalidTableCell(column.index, "descriptor repeat must be 0 or 1"),
          )
        }
        match column.max_elements {
          Some(maximum) if maximum < 0 =>
            return Err(
              InvalidTableCell(
                column.index,
                "heap maximum must be non-negative",
              ),
            )
          _ => ()
        }
        match element {
          Variable32(_) | Variable64(_) =>
            return Err(
              InvalidTableCell(column.index, "nested descriptors are invalid"),
            )
          _ => ()
        }
      }
      _ =>
        match column.max_elements {
          Some(_) =>
            return Err(
              InvalidTableCell(
                column.index,
                "fixed field cannot have a heap maximum",
              ),
            )
          None => ()
        }
    }
    if column.offset != offset {
      return Err(
        InvalidTableCell(
          column.index,
          "expected byte offset \{offset}, got \{column.offset}",
        ),
      )
    }
    let width = match table_column_width(column.kind, column.repeat) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    if column.width != width {
      return Err(
        InvalidTableCell(
          column.index,
          "expected width \{width}, got \{column.width}",
        ),
      )
    }
    offset = match checked_sum(offset, width, "binary-table row width") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
  }
  if offset != table.row_length {
    return Err(InvalidTableRowWidth(table.row_length, offset))
  }
  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 rows_end = match
    checked_sum(table.data_offset, row_bytes, "binary-table row end") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if table.heap_offset < rows_end || table.heap_length < 0 {
    return Err(InvalidTableCell(0, "heap overlaps rows or has negative length"))
  }
  match
    checked_sum(table.heap_offset, table.heap_length, "binary-table heap end") {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  Ok(())
}

///|
fn table_element_kind(
  code : Char,
  keyword : String,
  format : String,
) -> Result[TableColumnKind, FitsError] {
  match code {
    'A' => Ok(Ascii)
    'L' => Ok(Logical)
    'X' => Ok(Bit)
    'B' => Ok(UnsignedByte)
    'I' => Ok(Int16)
    'J' => Ok(Int32)
    'K' => Ok(Int64)
    'E' => Ok(Float32)
    'D' => Ok(Float64)
    _ => Err(UnsupportedTableFormat(keyword, format))
  }
}

///|
fn parse_table_column(
  header : Header,
  index : Int,
  offset : Int,
) -> Result[TableColumn, FitsError] {
  let format_keyword = "TFORM\{index}"
  let format = match header.get(format_keyword) {
    None => return Err(MissingKeyword(format_keyword))
    Some(card) =>
      match parse_card_string(card) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
  }
  let chars = format.to_array()
  if chars.length() == 0 {
    return Err(UnsupportedTableFormat(format_keyword, format))
  }
  let mut position = 0
  let mut repeat = 0
  while position < chars.length() &&
        chars[position] >= '0' &&
        chars[position] <= '9' {
    let digit = chars[position].to_int() - '0'.to_int()
    if repeat > (2147483647 - digit) / 10 {
      return Err(SizeOverflow("binary-table repeat count"))
    }
    repeat = repeat * 10 + digit
    position = position + 1
  }
  if position == 0 {
    repeat = 1
  }
  if position >= chars.length() {
    return Err(UnsupportedTableFormat(format_keyword, format))
  }
  let code = chars[position]
  let (kind, max_elements) = if code == 'P' || code == 'Q' {
    if repeat > 1 || position + 1 >= chars.length() {
      return Err(UnsupportedTableFormat(format_keyword, format))
    }
    let element = match
      table_element_kind(chars[position + 1], format_keyword, format) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    position = position + 2
    let maximum = if position == chars.length() {
      None
    } else {
      if chars[position] != '(' ||
        chars[chars.length() - 1] != ')' ||
        position + 2 >= chars.length() {
        return Err(UnsupportedTableFormat(format_keyword, format))
      }
      position = position + 1
      let mut value = 0
      while position < chars.length() - 1 {
        let digit = chars[position].to_int() - '0'.to_int()
        if digit < 0 || digit > 9 {
          return Err(UnsupportedTableFormat(format_keyword, format))
        }
        if value > (2147483647 - digit) / 10 {
          return Err(SizeOverflow("binary-table maximum element count"))
        }
        value = value * 10 + digit
        position = position + 1
      }
      Some(value)
    }
    (
      if code == 'P' {
        Variable32(element)
      } else {
        Variable64(element)
      },
      maximum,
    )
  } else {
    if position + 1 != chars.length() {
      return Err(UnsupportedTableFormat(format_keyword, format))
    }
    let element = match table_element_kind(code, format_keyword, format) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    (element, None)
  }
  let width = match table_column_width(kind, repeat) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let name_keyword = "TTYPE\{index}"
  let name = match header.get(name_keyword) {
    None => None
    Some(card) =>
      match parse_card_string(card) {
        Err(error) => return Err(error)
        Ok(value) => Some(value)
      }
  }
  Ok({ index, name, format, repeat, offset, width, kind, max_elements, })
}

///|
/// Parse the schema and heap geometry of a `BINTABLE` extension.
///
/// Supports fixed `A/L/X/B/I/J/K/E/D` fields and `P/Q` descriptors for these
/// element types. Complex values remain unsupported.
pub fn parse_binary_table(hdu : Hdu) -> Result[BinaryTable, FitsError] {
  match hdu.kind {
    OtherExtension("BINTABLE") => ()
    _ => return Err(NotBinaryTable(hdu.index))
  }
  let row_length = match required_table_integer(hdu.header, "NAXIS1") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let row_count = match required_table_integer(hdu.header, "NAXIS2") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let field_count = match required_table_integer(hdu.header, "TFIELDS") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if row_length < 0 {
    return Err(InvalidParameter("NAXIS1", row_length))
  }
  if row_count < 0 {
    return Err(InvalidParameter("NAXIS2", row_count))
  }
  if field_count < 0 || field_count > 999 {
    return Err(InvalidParameter("TFIELDS", field_count))
  }
  for (keyword, expected) in [("BITPIX", 8), ("NAXIS", 2), ("GCOUNT", 1)] {
    let actual = match required_table_integer(hdu.header, keyword) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    if actual != expected {
      return Err(InvalidParameter(keyword, actual))
    }
  }
  let columns = []
  let mut offset = 0
  for index = 1; index <= field_count; index = index + 1 {
    let column = match parse_table_column(hdu.header, index, offset) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    offset = match checked_sum(offset, column.width, "binary-table row width") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    columns.push(column)
  }
  if offset != row_length {
    return Err(InvalidTableRowWidth(row_length, offset))
  }
  let row_bytes = match
    checked_product(row_length, row_count, "binary-table row area") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if row_bytes > hdu.data_length {
    return Err(InvalidDataLength(row_bytes, hdu.data_length))
  }
  let pcount = match required_table_integer(hdu.header, "PCOUNT") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let expected_length = match
    checked_sum(row_bytes, pcount, "binary-table data length") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if expected_length != hdu.data_length {
    return Err(InvalidDataLength(expected_length, hdu.data_length))
  }
  let theap = match hdu.header.get("THEAP") {
    None => row_bytes
    Some(card) =>
      match parse_card_integer(card) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
  }
  if theap < row_bytes || theap > hdu.data_length {
    return Err(InvalidParameter("THEAP", theap))
  }
  let heap_offset = match checked_sum(hdu.data_offset, theap, "heap offset") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let table = {
    columns,
    row_length,
    row_count,
    data_offset: hdu.data_offset,
    heap_offset,
    heap_length: hdu.data_length - theap,
  }
  match validate_table_columns(table) {
    Err(error) => Err(error)
    Ok(_) => Ok(table)
  }
}

///|
fn decode_ascii_cell(
  data : Bytes,
  offset : Int,
  width : Int,
) -> Result[String, FitsError] {
  let output = StringBuilder()
  for index = 0; index < width; index = index + 1 {
    let byte = data[offset + index]
    if byte < b'\x20' || byte > b'\x7E' {
      return Err(NonAscii(offset + index, byte))
    }
    output.write_char(byte.to_int().to_char().unwrap())
  }
  Ok(output.to_string().trim_end(chars=" ").to_owned())
}

///|
fn decode_fixed_cell(
  data : Bytes,
  start : Int,
  kind : TableColumnKind,
  count : Int,
  column_index : Int,
) -> Result[TableValue, FitsError] {
  match kind {
    Ascii =>
      match decode_ascii_cell(data, start, count) {
        Err(error) => return Err(error)
        Ok(value) => Ok(Text(value))
      }
    Logical => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        match data[start + index] {
          b'T' => values.push(Some(true))
          b'F' => values.push(Some(false))
          b'\x00' => values.push(None)
          byte =>
            return Err(
              InvalidTableCell(
                column_index,
                "illegal logical byte \{byte.to_int()} at element \{index}",
              ),
            )
        }
      }
      Ok(Logicals(values))
    }
    Bit => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        let byte = data[start + index / 8]
        let mask = 1 << (7 - index % 8)
        values.push((byte.to_int() & mask) != 0)
      }
      Ok(Bits(values))
    }
    UnsignedByte => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        values.push(data[start + index].to_int64())
      }
      Ok(Integers(values))
    }
    Int16 => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        values.push(read_int16_be(data, start + index * 2))
      }
      Ok(Integers(values))
    }
    Int32 => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        values.push(read_int32_be(data, start + index * 4))
      }
      Ok(Integers(values))
    }
    Int64 => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        values.push(read_int64_be(data, start + index * 8))
      }
      Ok(Integers(values))
    }
    Float32 => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        values.push(read_float32_be(data, start + index * 4))
      }
      Ok(Reals(values))
    }
    Float64 => {
      let values = []
      for index = 0; index < count; index = index + 1 {
        values.push(read_float64_be(data, start + index * 8))
      }
      Ok(Reals(values))
    }
    Variable32(_) | Variable64(_) =>
      Err(InvalidTableCell(column_index, "nested descriptors are invalid"))
  }
}

///|
/// Decode one zero-based binary-table row into typed cells.
pub fn decode_binary_row(
  data : Bytes,
  table : BinaryTable,
  row_index : Int,
) -> Result[Array[TableValue], FitsError] {
  match validate_table_columns(table) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if row_index < 0 || row_index >= table.row_count {
    return Err(TableRowOutOfRange(row_index, table.row_count))
  }
  let relative = match
    checked_product(row_index, table.row_length, "binary-table row offset") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let row_offset = match
    checked_sum(table.data_offset, relative, "binary-table absolute row offset") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if row_offset < 0 || row_offset > data.length() {
    return Err(Truncated(row_offset, table.row_length, 0))
  }
  let available = data.length() - row_offset
  if table.row_length < 0 || table.row_length > available {
    return Err(Truncated(row_offset, table.row_length, available))
  }
  let cells = []
  for column in table.columns {
    let field_end = match
      checked_sum(column.offset, column.width, "binary-table field boundary") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    if column.offset < 0 || column.width < 0 || field_end > table.row_length {
      return Err(InvalidTableRowWidth(table.row_length, field_end))
    }
    let start = row_offset + column.offset
    let value = match column.kind {
      Variable32(element) | Variable64(element) =>
        decode_heap_cell(data, table, row_index, column, start, element)
      kind => decode_fixed_cell(data, start, kind, column.repeat, column.index)
    }
    match value {
      Err(error) => return Err(error)
      Ok(cell) => cells.push(cell)
    }
  }
  Ok(cells)
}

///|
fn invalid_cell_type(column : TableColumn) -> FitsError {
  let expected = match column.kind {
    Ascii => "text"
    Logical => "logical array"
    Bit => "bit array"
    UnsignedByte | Int16 | Int32 | Int64 => "integer array"
    Float32 | Float64 => "real array"
    Variable32(_) | Variable64(_) => "heap-backed array"
  }
  InvalidTableCell(column.index, "expected \{expected}")
}

///|
fn encode_logical_cell(
  output : Array[Byte],
  column : TableColumn,
  values : Array[Bool?],
) -> Result[Unit, FitsError] {
  if values.length() != column.repeat {
    return Err(
      InvalidTableCell(
        column.index,
        "requires \{column.repeat} logical values, got \{values.length()}",
      ),
    )
  }
  for value in values {
    output.push(
      match value {
        Some(true) => b'T'
        Some(false) => b'F'
        None => b'\x00'
      },
    )
  }
  Ok(())
}

///|
fn encode_ascii_cell(
  output : Array[Byte],
  column : TableColumn,
  text : String,
) -> Result[Unit, FitsError] {
  let chars = text.to_array()
  if chars.length() > column.repeat {
    return Err(
      InvalidTableCell(
        column.index,
        "text has \{chars.length()} characters; maximum is \{column.repeat}",
      ),
    )
  }
  for ch in chars {
    let value = ch.to_int()
    if value < 0x20 || value > 0x7e {
      return Err(InvalidTableCell(column.index, "text must be printable ASCII"))
    }
    output.push(value.to_byte())
  }
  for index = chars.length(); index < column.repeat; index = index + 1 {
    output.push(b' ')
  }
  Ok(())
}

///|
fn encode_integer_cell(
  output : Array[Byte],
  column : TableColumn,
  values : Array[Int64],
) -> Result[Unit, FitsError] {
  if values.length() != column.repeat {
    return Err(
      InvalidTableCell(
        column.index,
        "requires \{column.repeat} integers, got \{values.length()}",
      ),
    )
  }
  let bitpix = match column.kind {
    UnsignedByte => 8
    Int16 => 16
    Int32 => 32
    Int64 => 64
    _ => return Err(invalid_cell_type(column))
  }
  let width = match table_column_width(column.kind, 1) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  for value in values {
    if !integer_value_fits(value, bitpix) {
      return Err(
        InvalidTableCell(
          column.index,
          "integer \{value} is outside the \{column.format} range",
        ),
      )
    }
    append_integer_be(output, value, width)
  }
  Ok(())
}

///|
fn encode_bit_cell(
  output : Array[Byte],
  column : TableColumn,
  values : Array[Bool],
) -> Result[Unit, FitsError] {
  if values.length() != column.repeat {
    return Err(
      InvalidTableCell(
        column.index,
        "requires \{column.repeat} bits, got \{values.length()}",
      ),
    )
  }
  for byte_index = 0; byte_index < column.width; byte_index = byte_index + 1 {
    let mut byte = 0
    for bit_index = 0; bit_index < 8; bit_index = bit_index + 1 {
      let value_index = byte_index * 8 + bit_index
      if value_index < values.length() && values[value_index] {
        byte = byte | (1 << (7 - bit_index))
      }
    }
    output.push(byte.to_byte())
  }
  Ok(())
}

///|
fn encode_real_cell(
  output : Array[Byte],
  column : TableColumn,
  values : Array[Double],
) -> Result[Unit, FitsError] {
  if values.length() != column.repeat {
    return Err(
      InvalidTableCell(
        column.index,
        "requires \{column.repeat} real values, got \{values.length()}",
      ),
    )
  }
  for index, value in values {
    match column.kind {
      Float32 => {
        let narrowed = Float::from_double(value)
        if !value.is_nan() && !value.is_inf() && narrowed.is_inf() {
          return Err(
            InvalidTableCell(
              column.index,
              "real value \{index} overflows binary32",
            ),
          )
        }
        append_uint32_be(output, narrowed.reinterpret_as_uint())
      }
      Float64 => append_uint64_be(output, value.reinterpret_as_uint64())
      _ => return Err(invalid_cell_type(column))
    }
  }
  Ok(())
}

///|
fn encode_fixed_cell(
  output : Array[Byte],
  column : TableColumn,
  cell : TableValue,
) -> Result[Unit, FitsError] {
  match column.kind {
    Ascii =>
      match cell {
        Text(text) => encode_ascii_cell(output, column, text)
        _ => Err(invalid_cell_type(column))
      }
    Logical =>
      match cell {
        Logicals(values) => encode_logical_cell(output, column, values)
        _ => Err(invalid_cell_type(column))
      }
    Bit =>
      match cell {
        Bits(values) => encode_bit_cell(output, column, values)
        _ => Err(invalid_cell_type(column))
      }
    UnsignedByte | Int16 | Int32 | Int64 =>
      match cell {
        Integers(values) => encode_integer_cell(output, column, values)
        _ => Err(invalid_cell_type(column))
      }
    Float32 | Float64 =>
      match cell {
        Reals(values) => encode_real_cell(output, column, values)
        _ => Err(invalid_cell_type(column))
      }
    Variable32(_) | Variable64(_) =>
      Err(UnsupportedTableFormat("TFORM\{column.index}", column.format))
  }
}

///|
/// Encode one fixed-width binary-table row in FITS network byte order.
///
/// Text is padded with spaces to its declared width. Numeric cells must supply
/// exactly the column repeat count, and narrowing conversions are checked.
pub fn encode_binary_row(
  table : BinaryTable,
  cells : Array[TableValue],
) -> Result[Bytes, FitsError] {
  match validate_table_columns(table) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if cells.length() != table.columns.length() {
    return Err(InvalidTableCellCount(table.columns.length(), cells.length()))
  }
  let output = Array(capacity=table.row_length)
  for index, column in table.columns {
    let result = encode_fixed_cell(output, column, cells[index])
    match result {
      Err(error) => return Err(error)
      Ok(_) => ()
    }
  }
  Ok(Bytes::from_array(output))
}

///|
/// Encode every row of a fixed-width binary table as one contiguous data unit.
pub fn encode_binary_rows(
  table : BinaryTable,
  rows : Array[Array[TableValue]],
) -> Result[Bytes, FitsError] {
  match validate_table_columns(table) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if table.row_count < 0 {
    return Err(InvalidParameter("NAXIS2", table.row_count))
  }
  if rows.length() != table.row_count {
    return Err(InvalidTableRowCount(table.row_count, rows.length()))
  }
  let byte_length = match
    checked_product(table.row_length, table.row_count, "binary-table row area") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let output = Array(capacity=byte_length)
  for row in rows {
    let encoded = match encode_binary_row(table, row) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    append_bytes(output, encoded)
  }
  Ok(Bytes::from_array(output))
}