///|
/// One 80-byte FITS header card.
///
/// `value` is the trimmed lexical value after the `= ` indicator. An empty
/// string represents the standard's undefined value. Cards without a value
/// indicator use `None` and can retain commentary in `comment`.
pub(all) struct Card {
  keyword : String
  value : String?
  comment : String?
  record_index : Int
} derive(Eq, Debug)

///|
/// A parsed FITS header and the absolute offset of its following data unit.
pub(all) struct Header {
  cards : Array[Card]
  data_offset : Int
} derive(Eq, Debug)

///|
/// The structural role of one FITS header/data unit.
pub(all) enum HduKind {
  Primary
  ImageExtension
  OtherExtension(String)
} derive(Eq, Debug)

///|
/// Checked geometry for a primary array or IMAGE extension.
pub(all) struct ImageLayout {
  bitpix : Int
  axes : Array[Int]
  pixel_count : Int
  byte_length : Int
} derive(Eq, Debug)

///|
/// One parsed FITS header/data unit with absolute byte boundaries.
pub(all) struct Hdu {
  index : Int
  kind : HduKind
  header : Header
  image : ImageLayout?
  data_offset : Int
  data_length : Int
  next_hdu_offset : Int
} derive(Eq, Debug)

///|
/// A structurally validated sequence of FITS HDUs.
pub(all) struct FitsFile {
  hdus : Array[Hdu]
} derive(Eq, Debug)

///|
/// Decoded integer image samples and their FITS linear-scaling metadata.
pub(all) struct IntegerImage {
  axes : Array[Int]
  raw : Array[Int64]
  bscale : Double
  bzero : Double
  blank : Int64?
} derive(Eq, Debug)

///|
/// Convert every non-blank raw sample to its physical FITS value.
pub fn IntegerImage::physical_values(self : IntegerImage) -> Array[Double?] {
  self.raw.map(fn(value) {
    match self.blank {
      Some(blank) if value == blank => None
      _ => Some(value.to_double() * self.bscale + self.bzero)
    }
  })
}

///|
/// Decoded IEEE floating-point image samples and FITS linear scaling.
pub(all) struct FloatingImage {
  axes : Array[Int]
  raw : Array[Double]
  bscale : Double
  bzero : Double
} derive(Eq, Debug)

///|
/// Apply FITS linear scaling while preserving IEEE special values.
pub fn FloatingImage::physical_values(self : FloatingImage) -> Array[Double] {
  self.raw.map(fn(value) { value * self.bscale + self.bzero })
}

///|
/// The fixed-width storage representation of one FITS binary-table column.
pub(all) enum TableColumnKind {
  Ascii
  Logical
  Bit
  UnsignedByte
  Int16
  Int32
  Int64
  Float32
  Float64
  Variable32(TableColumnKind)
  Variable64(TableColumnKind)
} derive(Eq, Debug)

///|
/// A validated FITS binary-table column, fixed-width or heap-backed.
pub(all) struct TableColumn {
  index : Int
  name : String?
  format : String
  repeat : Int
  offset : Int
  width : Int
  kind : TableColumnKind
  max_elements : Int?
} derive(Eq, Debug)

///|
/// User-facing schema for constructing a binary-table extension.
pub(all) struct TableField {
  name : String?
  format : String
} derive(Eq, Debug)

///|
/// Row and heap geometry plus column schema for a BINTABLE extension.
pub(all) struct BinaryTable {
  columns : Array[TableColumn]
  row_length : Int
  row_count : Int
  data_offset : Int
  heap_offset : Int
  heap_length : Int
} derive(Eq, Debug)

///|
/// One decoded binary-table cell.
pub(all) enum TableValue {
  Text(String)
  Logicals(Array[Bool?])
  Bits(Array[Bool])
  Integers(Array[Int64])
  Reals(Array[Double])
} derive(Eq, Debug)

///|
/// Source-aware failures from FITS structural parsing.
pub(all) enum FitsError {
  Truncated(Int, Int, Int)
  NonAscii(Int, Byte)
  InvalidKeyword(Int, String)
  InvalidCard(Int, String)
  MissingEnd(Int)
  InvalidPadding(Int, Byte)
  MissingKeyword(String)
  InvalidInteger(String, String)
  InvalidLogical(String, String)
  InvalidString(String, String)
  InvalidRequiredOrder(Int, String, String)
  UnsupportedBitpix(Int)
  InvalidAxis(Int, Int)
  InvalidParameter(String, Int)
  SizeOverflow(String)
  NotImageHdu(Int)
  UnsupportedIntegerBitpix(Int)
  UnsupportedFloatingBitpix(Int)
  UnsupportedImageLayout(Int)
  InvalidReal(String, String)
  CardTooLong(Int, Int)
  InvalidSampleCount(Int, Int)
  PixelOutOfRange(Int, Int64, Int)
  BlankOutOfRange(Int64, Int)
  FloatingPixelOverflow(Int, Int)
  InvalidDataLength(Int, Int)
  NotBinaryTable(Int)
  UnsupportedTableFormat(String, String)
  InvalidTableRowWidth(Int, Int)
  TableRowOutOfRange(Int, Int)
  InvalidTableRowCount(Int, Int)
  InvalidTableCellCount(Int, Int)
  InvalidTableCell(Int, String)
  InvalidHeapDescriptor(Int, Int, String)
} derive(Eq, Debug)

///|
pub fn FitsError::message(self : FitsError) -> String {
  match self {
    Truncated(offset, needed, available) =>
      "truncated FITS data at byte \{offset}: need \{needed} bytes, have \{available}"
    NonAscii(offset, byte) =>
      "non-ASCII byte \{byte.to_int()} in FITS header at byte \{offset}"
    InvalidKeyword(record, keyword) =>
      "invalid FITS keyword '\{keyword}' in header record \{record + 1}"
    InvalidCard(record, reason) =>
      "invalid FITS card in header record \{record + 1}: \{reason}"
    MissingEnd(offset) =>
      "FITS header starting at byte \{offset} has no END card"
    InvalidPadding(offset, byte) =>
      "invalid FITS header padding byte \{byte.to_int()} at byte \{offset}"
    MissingKeyword(keyword) => "required FITS keyword '\{keyword}' is missing"
    InvalidInteger(keyword, value) =>
      "FITS keyword '\{keyword}' does not contain an integer: '\{value}'"
    InvalidLogical(keyword, value) =>
      "FITS keyword '\{keyword}' does not contain a logical value: '\{value}'"
    InvalidString(keyword, value) =>
      "FITS keyword '\{keyword}' does not contain a valid quoted string: '\{value}'"
    InvalidRequiredOrder(record, expected, found) =>
      "FITS header record \{record + 1} must be '\{expected}', found '\{found}'"
    UnsupportedBitpix(value) => "unsupported FITS BITPIX value \{value}"
    InvalidAxis(axis, value) =>
      "FITS NAXIS\{axis} must be non-negative, got \{value}"
    InvalidParameter(keyword, value) =>
      "FITS keyword '\{keyword}' has invalid value \{value}"
    SizeOverflow(context) => "FITS \{context} exceeds addressable input size"
    NotImageHdu(index) => "FITS HDU \{index} is not an image"
    UnsupportedIntegerBitpix(value) =>
      "FITS BITPIX \{value} is not an integer pixel representation"
    UnsupportedFloatingBitpix(value) =>
      "FITS BITPIX \{value} is not an IEEE floating-point representation"
    UnsupportedImageLayout(index) =>
      "FITS HDU \{index} uses random groups or parameters and cannot be decoded as a contiguous image"
    InvalidReal(keyword, value) =>
      "FITS keyword '\{keyword}' does not contain a finite real value: '\{value}'"
    CardTooLong(record, length) =>
      "encoded FITS header record \{record + 1} is \{length} bytes; maximum is 80"
    InvalidSampleCount(expected, actual) =>
      "FITS image shape requires \{expected} samples, got \{actual}"
    PixelOutOfRange(index, value, bitpix) =>
      "pixel \{index} value \{value} is outside the BITPIX=\{bitpix} range"
    BlankOutOfRange(value, bitpix) =>
      "BLANK value \{value} is outside the BITPIX=\{bitpix} range"
    FloatingPixelOverflow(index, bitpix) =>
      "pixel \{index} is finite but overflows BITPIX=\{bitpix}"
    InvalidDataLength(expected, actual) =>
      "FITS header requires \{expected} data bytes, got \{actual}"
    NotBinaryTable(index) => "FITS HDU \{index} is not a binary table"
    UnsupportedTableFormat(keyword, format) =>
      "FITS keyword '\{keyword}' uses unsupported binary-table format '\{format}'"
    InvalidTableRowWidth(expected, actual) =>
      "FITS binary-table row declares \{expected} bytes but its columns occupy \{actual}"
    TableRowOutOfRange(index, count) =>
      "FITS binary-table row \{index} is outside the \{count}-row table"
    InvalidTableRowCount(expected, actual) =>
      "FITS binary table requires \{expected} rows, got \{actual}"
    InvalidTableCellCount(expected, actual) =>
      "FITS binary-table row requires \{expected} cells, got \{actual}"
    InvalidTableCell(column, reason) =>
      "invalid FITS binary-table column \{column}: \{reason}"
    InvalidHeapDescriptor(column, row, reason) =>
      "invalid FITS heap descriptor in column \{column}, row \{row}: \{reason}"
  }
}

///|
/// Explicitly preserve derived methods under the current MoonBit toolchain.
pub extend Card with Eq::{not_equal, equal}

///|
pub extend Card with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend Header with Eq::{not_equal, equal}

///|
pub extend Header with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend HduKind with Eq::{not_equal, equal}

///|
pub extend HduKind with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend ImageLayout with Eq::{not_equal, equal}

///|
pub extend ImageLayout with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend Hdu with Eq::{not_equal, equal}

///|
pub extend Hdu with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend FitsFile with Eq::{not_equal, equal}

///|
pub extend FitsFile with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend IntegerImage with Eq::{not_equal, equal}

///|
pub extend IntegerImage with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend FloatingImage with Eq::{not_equal, equal}

///|
pub extend FloatingImage with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend TableColumnKind with Eq::{not_equal, equal}

///|
pub extend TableColumnKind with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend TableColumn with Eq::{not_equal, equal}

///|
pub extend TableColumn with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend TableField with Eq::{not_equal, equal}

///|
pub extend TableField with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend BinaryTable with Eq::{not_equal, equal}

///|
pub extend BinaryTable with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend TableValue with Eq::{not_equal, equal}

///|
pub extend TableValue with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend FitsError with Eq::{not_equal, equal}

///|
pub extend FitsError with @moonbitlang/core/debug.Debug::{to_repr}