///|
fn parse_card_int64(card : Card) -> Result[Int64, FitsError] {
  let text = match card_lexical_value(card) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let chars = text.to_array()
  if chars.length() == 0 {
    return Err(InvalidInteger(card.keyword, text))
  }
  let mut index = 0
  let mut negative = false
  if chars[0] == '-' || chars[0] == '+' {
    negative = chars[0] == '-'
    index = 1
  }
  if index == chars.length() {
    return Err(InvalidInteger(card.keyword, text))
  }
  let minimum = -9223372036854775807L - 1L
  let limit = if negative { minimum } else { -9223372036854775807L }
  let multiply_limit = limit / 10L
  let mut value = 0L
  while index < chars.length() {
    let ch = chars[index]
    if ch < '0' || ch > '9' {
      return Err(InvalidInteger(card.keyword, text))
    }
    let digit = (ch.to_int() - '0'.to_int()).to_int64()
    if value < multiply_limit {
      return Err(SizeOverflow("integer keyword \{card.keyword}"))
    }
    value = value * 10L
    if value < limit + digit {
      return Err(SizeOverflow("integer keyword \{card.keyword}"))
    }
    value = value - digit
    index = index + 1
  }
  Ok(if negative { value } else { -value })
}

///|
fn parse_card_real(card : Card) -> Result[Double, FitsError] {
  let text = match card_lexical_value(card) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let chars = text.to_array()
  if chars.length() == 0 {
    return Err(InvalidReal(card.keyword, text))
  }
  let mut index = 0
  let mut negative = false
  if chars[0] == '-' || chars[0] == '+' {
    negative = chars[0] == '-'
    index = 1
  }
  let mut value = 0.0
  let mut digits = 0
  let mut fractional_digits = 0
  let mut after_point = false
  while index < chars.length() {
    let ch = chars[index]
    if ch >= '0' && ch <= '9' {
      value = value * 10.0 + (ch.to_int() - '0'.to_int()).to_double()
      digits = digits + 1
      if after_point {
        fractional_digits = fractional_digits + 1
      }
      index = index + 1
    } else if ch == '.' && !after_point {
      after_point = true
      index = index + 1
    } else {
      break
    }
  }
  if digits == 0 {
    return Err(InvalidReal(card.keyword, text))
  }
  let mut exponent = 0
  let mut exponent_negative = false
  if index < chars.length() &&
    (
      chars[index] == 'E' ||
      chars[index] == 'e' ||
      chars[index] == 'D' ||
      chars[index] == 'd'
    ) {
    index = index + 1
    if index < chars.length() && (chars[index] == '-' || chars[index] == '+') {
      exponent_negative = chars[index] == '-'
      index = index + 1
    }
    let exponent_start = index
    while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
      let digit = chars[index].to_int() - '0'.to_int()
      if exponent > 1000 {
        return Err(InvalidReal(card.keyword, text))
      }
      exponent = exponent * 10 + digit
      index = index + 1
    }
    if index == exponent_start {
      return Err(InvalidReal(card.keyword, text))
    }
  }
  if index != chars.length() {
    return Err(InvalidReal(card.keyword, text))
  }
  if exponent_negative {
    exponent = -exponent
  }
  exponent = exponent - fractional_digits
  if exponent > 308 || exponent < -324 {
    return Err(InvalidReal(card.keyword, text))
  }
  if exponent > 0 {
    for power = 0; power < exponent; power = power + 1 {
      value = value * 10.0
    }
  } else {
    for power = 0; power > exponent; power = power - 1 {
      value = value / 10.0
    }
  }
  if value > 1.7976931348623157e308 {
    return Err(InvalidReal(card.keyword, text))
  }
  Ok(if negative { -value } else { value })
}

///|
fn scaling_value(
  header : Header,
  keyword : String,
  default : Double,
) -> Result[Double, FitsError] {
  match header.get(keyword) {
    None => Ok(default)
    Some(card) => parse_card_real(card)
  }
}

///|
fn read_int16_be(data : Bytes, offset : Int) -> Int64 {
  let unsigned = data[offset].to_int() * 256 + data[offset + 1].to_int()
  (if unsigned >= 32768 { unsigned - 65536 } else { unsigned }).to_int64()
}

///|
fn read_int32_be(data : Bytes, offset : Int) -> Int64 {
  let unsigned = data[offset].to_int64() * 16777216L +
    data[offset + 1].to_int64() * 65536L +
    data[offset + 2].to_int64() * 256L +
    data[offset + 3].to_int64()
  if unsigned >= 2147483648L {
    unsigned - 4294967296L
  } else {
    unsigned
  }
}

///|
fn read_int64_be(data : Bytes, offset : Int) -> Int64 {
  let mut value = 0L
  for index = 0; index < 8; index = index + 1 {
    value = (value << 8) | data[offset + index].to_int64()
  }
  value
}

///|
/// Decode a contiguous integer primary array or IMAGE extension.
///
/// FITS stores samples in big-endian order. `BITPIX=8` is unsigned; 16, 32,
/// and 64-bit samples use two's-complement signed integers. Linear scaling and
/// blank detection are retained explicitly rather than changing the raw data.
pub fn decode_integer_image(
  data : Bytes,
  hdu : Hdu,
) -> Result[IntegerImage, FitsError] {
  let layout = match hdu.image {
    None => return Err(NotImageHdu(hdu.index))
    Some(value) => value
  }
  let bytes_per_pixel = match layout.bitpix {
    8 => 1
    16 => 2
    32 => 4
    64 => 8
    value => return Err(UnsupportedIntegerBitpix(value))
  }
  let expected_length = match
    checked_product(layout.pixel_count, bytes_per_pixel, "integer image") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if expected_length != hdu.data_length {
    return Err(UnsupportedImageLayout(hdu.index))
  }
  if hdu.data_offset < 0 || expected_length > data.length() - hdu.data_offset {
    return Err(
      Truncated(
        hdu.data_offset,
        expected_length,
        data.length() - hdu.data_offset,
      ),
    )
  }
  let bscale = match scaling_value(hdu.header, "BSCALE", 1.0) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let bzero = match scaling_value(hdu.header, "BZERO", 0.0) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let blank = match hdu.header.get("BLANK") {
    None => None
    Some(card) =>
      match parse_card_int64(card) {
        Err(error) => return Err(error)
        Ok(value) => Some(value)
      }
  }
  let raw = []
  for index = 0; index < layout.pixel_count; index = index + 1 {
    let offset = hdu.data_offset + index * bytes_per_pixel
    raw.push(
      match layout.bitpix {
        8 => data[offset].to_int64()
        16 => read_int16_be(data, offset)
        32 => read_int32_be(data, offset)
        64 => read_int64_be(data, offset)
        _ => abort("validated integer BITPIX became unreachable")
      },
    )
  }
  Ok({ axes: layout.axes, raw, bscale, bzero, blank, })
}

///|
fn read_float32_be(data : Bytes, offset : Int) -> Double {
  let bits = (data[offset].to_uint() << 24) |
    (data[offset + 1].to_uint() << 16) |
    (data[offset + 2].to_uint() << 8) |
    data[offset + 3].to_uint()
  Float::reinterpret_from_uint(bits).to_double()
}

///|
fn read_float64_be(data : Bytes, offset : Int) -> Double {
  let mut bits = 0UL
  for index = 0; index < 8; index = index + 1 {
    bits = (bits << 8) | data[offset + index].to_uint64()
  }
  bits.reinterpret_as_double()
}

///|
/// Decode a contiguous IEEE floating-point primary array or IMAGE extension.
///
/// `BITPIX=-32` values are widened exactly from IEEE binary32 to `Double`;
/// `BITPIX=-64` values retain their binary64 representation. NaN, infinities,
/// and signed zero are preserved in `raw` before FITS linear scaling.
pub fn decode_floating_image(
  data : Bytes,
  hdu : Hdu,
) -> Result[FloatingImage, FitsError] {
  let layout = match hdu.image {
    None => return Err(NotImageHdu(hdu.index))
    Some(value) => value
  }
  let bytes_per_pixel = match layout.bitpix {
    -32 => 4
    -64 => 8
    value => return Err(UnsupportedFloatingBitpix(value))
  }
  let expected_length = match
    checked_product(layout.pixel_count, bytes_per_pixel, "floating image") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if expected_length != hdu.data_length {
    return Err(UnsupportedImageLayout(hdu.index))
  }
  if hdu.data_offset < 0 || hdu.data_offset > data.length() {
    return Err(Truncated(hdu.data_offset, expected_length, 0))
  }
  let available = data.length() - hdu.data_offset
  if expected_length > available {
    return Err(Truncated(hdu.data_offset, expected_length, available))
  }
  let bscale = match scaling_value(hdu.header, "BSCALE", 1.0) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let bzero = match scaling_value(hdu.header, "BZERO", 0.0) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let raw = []
  for index = 0; index < layout.pixel_count; index = index + 1 {
    let offset = hdu.data_offset + index * bytes_per_pixel
    raw.push(
      match layout.bitpix {
        -32 => read_float32_be(data, offset)
        -64 => read_float64_be(data, offset)
        _ => abort("validated floating BITPIX became unreachable")
      },
    )
  }
  Ok({ axes: layout.axes, raw, bscale, bzero, })
}

///|
fn integer_value_fits(value : Int64, bitpix : Int) -> Bool {
  match bitpix {
    8 => value >= 0L && value <= 255L
    16 => value >= -32768L && value <= 32767L
    32 => value >= -2147483648L && value <= 2147483647L
    64 => true
    _ => false
  }
}

///|
fn image_sample_count(axes : Array[Int]) -> Result[Int, FitsError] {
  if axes.length() == 0 {
    return Ok(0)
  }
  let mut count = 1
  for index, length in axes {
    if length < 0 {
      return Err(InvalidAxis(index + 1, length))
    }
    count = match checked_product(count, length, "pixel count") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
  }
  Ok(count)
}

///|
fn append_integer_be(output : Array[Byte], value : Int64, width : Int) -> Unit {
  let bits = value.reinterpret_as_uint64()
  for index = 0; index < width; index = index + 1 {
    let shift = (width - index - 1) * 8
    output.push((bits >> shift).to_byte())
  }
}

///|
/// Encode raw integer image samples in FITS big-endian byte order.
///
/// The declared axes must contain exactly one entry per raw sample. Values and
/// an optional `BLANK` sentinel are range-checked for the requested positive
/// integer `BITPIX`; no clipping or scaling is performed.
pub fn encode_integer_pixels(
  image : IntegerImage,
  bitpix : Int,
) -> Result[Bytes, FitsError] {
  let bytes_per_pixel = match bitpix {
    8 => 1
    16 => 2
    32 => 4
    64 => 8
    value => return Err(UnsupportedIntegerBitpix(value))
  }
  let expected = match image_sample_count(image.axes) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if expected != image.raw.length() {
    return Err(InvalidSampleCount(expected, image.raw.length()))
  }
  match image.blank {
    Some(value) if !integer_value_fits(value, bitpix) =>
      return Err(BlankOutOfRange(value, bitpix))
    _ => ()
  }
  let byte_length = match
    checked_product(expected, bytes_per_pixel, "encoded integer image") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let output = Array(capacity=byte_length)
  for index, value in image.raw {
    if !integer_value_fits(value, bitpix) {
      return Err(PixelOutOfRange(index, value, bitpix))
    }
    append_integer_be(output, value, bytes_per_pixel)
  }
  Ok(Bytes::from_array(output))
}

///|
fn append_uint32_be(output : Array[Byte], bits : UInt) -> Unit {
  output.push((bits >> 24).to_byte())
  output.push((bits >> 16).to_byte())
  output.push((bits >> 8).to_byte())
  output.push(bits.to_byte())
}

///|
fn append_uint64_be(output : Array[Byte], bits : UInt64) -> Unit {
  for index = 0; index < 8; index = index + 1 {
    let shift = (7 - index) * 8
    output.push((bits >> shift).to_byte())
  }
}

///|
/// Encode floating-point image samples in FITS big-endian byte order.
///
/// `BITPIX=-32` performs the standard nearest binary32 conversion and rejects
/// finite values that would become infinity. `BITPIX=-64` preserves every
/// binary64 bit. NaN, infinities, and signed zero remain valid FITS samples.
pub fn encode_floating_pixels(
  image : FloatingImage,
  bitpix : Int,
) -> Result[Bytes, FitsError] {
  let bytes_per_pixel = match bitpix {
    -32 => 4
    -64 => 8
    value => return Err(UnsupportedFloatingBitpix(value))
  }
  let expected = match image_sample_count(image.axes) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if expected != image.raw.length() {
    return Err(InvalidSampleCount(expected, image.raw.length()))
  }
  let byte_length = match
    checked_product(expected, bytes_per_pixel, "encoded floating image") {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let output = Array(capacity=byte_length)
  for index, value in image.raw {
    if bitpix == -32 {
      let narrowed = Float::from_double(value)
      if !value.is_nan() && !value.is_inf() && narrowed.is_inf() {
        return Err(FloatingPixelOverflow(index, bitpix))
      }
      append_uint32_be(output, narrowed.reinterpret_as_uint())
    } else {
      append_uint64_be(output, value.reinterpret_as_uint64())
    }
  }
  Ok(Bytes::from_array(output))
}