///|
pub(all) enum Space {
  QForm
  SForm
  PreferSForm
} derive(Eq)

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

///|
/// Storage is private. Images own their input bytes; methods return copies.
pub struct Image {
  priv header : Bytes
  priv payload : Bytes
  priv shape : FixedArray[Int]
  priv ndim : Int
  priv dtype : Int
  priv width : Int
  priv endian : Endian
  priv slope : Double
  priv intercept : Double
  priv spacing : FixedArray[Double]
  priv qform : Affine?
  priv sform : Affine?
  priv qcode : Int
  priv scode : Int
  priv spatial_unit : Int
  priv time_unit : Int
  priv extension_count : Int
  priv intent : Int
}

///|
pub fn Image::dimensions(self : Image) -> Array[Int] {
  Array::makei(self.ndim, i => self.shape[i])
}

///|
pub fn Image::datatype(self : Image) -> Int {
  self.dtype
}

///|
pub fn Image::byte_order(self : Image) -> Endian {
  self.endian
}

///|
pub fn Image::voxel_count(self : Image) -> Int {
  self.payload.length() / self.width
}

///|
pub fn Image::scaling(self : Image) -> (Double, Double) {
  (self.slope, self.intercept)
}

///|
pub fn Image::voxel_spacing(self : Image) -> Array[Double] {
  Array::makei(self.ndim, i => self.spacing[i])
}

///|
pub fn Image::units(self : Image) -> (Int, Int) {
  (self.spatial_unit, self.time_unit)
}

///|
pub fn Image::form_codes(self : Image) -> (Int, Int) {
  (self.qcode, self.scode)
}

///|
pub fn Image::extensions(self : Image) -> Int {
  self.extension_count
}

///|
/// Unknown space is explicit; there is no invented fallback affine.
pub fn Image::affine(self : Image, space : Space) -> Affine? {
  match space {
    QForm => self.qform
    SForm => self.sform
    PreferSForm =>
      match self.sform {
        Some(_) => self.sform
        None => self.qform
      }
  }
}

///|
pub fn Image::world(
  self : Image,
  x : Double,
  y : Double,
  z : Double,
  space? : Space = PreferSForm,
) -> Array[Double] raise NiftiError {
  match self.affine(space) {
    Some(a) => a.transform(x, y, z)
    None => raise NiftiError("unknown_space", -1)
  }
}

///|
/// Stored samples are decoded without scl_slope/scl_inter.
pub fn Image::raw_at(self : Image, index : Int) -> Double raise NiftiError {
  require(index >= 0 && index < self.voxel_count(), "voxel_index")
  let r : Reader = { bytes: self.payload, endian: self.endian, }
  let p = index * self.width
  match self.dtype {
    2 => r.bytes[p].to_double()
    256 => {
      let v = r.bytes[p].to_int()
      (if v >= 128 { v - 256 } else { v }).to_double()
    }
    4 => r.i16(p).to_double()
    512 => r.bits(p, 2).to_double()
    8 => r.i32(p).to_double()
    768 => r.bits(p, 4).to_double()
    16 => r.f32(p)
    64 => r.f64(p)
    _ => raise NiftiError("unsupported_datatype", 70)
  }
}

///|
pub fn Image::scaled_at(self : Image, index : Int) -> Double raise NiftiError {
  self.raw_at(index) * self.slope + self.intercept
}

///|
pub fn Image::linear_index(
  self : Image,
  x : Int,
  y : Int,
  z : Int,
  t? : Int = 0,
) -> Int raise NiftiError {
  let indices = [x, y, z, t]
  for a in 0..<4 {
    require(indices[a] >= 0 && indices[a] < self.shape[a], "voxel_index")
  }
  x + self.shape[0] * (y + self.shape[1] * (z + self.shape[2] * t))
}

///|
pub fn Image::voxel(
  self : Image,
  x : Int,
  y : Int,
  z : Int,
  t? : Int = 0,
  scaled? : Bool = true,
) -> Double raise NiftiError {
  let i = self.linear_index(x, y, z, t~)
  if scaled {
    self.scaled_at(i)
  } else {
    self.raw_at(i)
  }
}

///|
/// Lossless within the declared file region. Trailing input bytes are rejected.
pub fn Image::to_bytes(self : Image) -> Bytes {
  self.header + self.payload
}