///|
/// Input metadata. Tensor construction and access perform deep copies.
/// Empty per-axis lists mean absent; nonempty lists must cover every axis.
pub(all) struct Metadata {
  content : String?
  labels : Array[String]
  units : Array[String]
  kinds : Array[String]
  spacings : Array[Double]
  space : String?
  space_dimension : Int32?
  origin : Array[Double]?
  directions : Array[Array[Double]?]
  custom : Map[String, String]
} derive(Debug, Eq)

///|
pub fn Metadata::empty() -> Metadata {
  {
    content: None,
    labels: [],
    units: [],
    kinds: [],
    spacings: [],
    space: None,
    space_dimension: None,
    origin: None,
    directions: [],
    custom: Map([]),
  }
}

///|
fn Metadata::copy_owned(self : Metadata) -> Metadata {
  {
    content: self.content,
    labels: self.labels.copy(),
    units: self.units.copy(),
    kinds: self.kinds.copy(),
    spacings: self.spacings.copy(),
    space: self.space,
    space_dimension: self.space_dimension,
    origin: self.origin.map(fn(v) { v.copy() }),
    directions: self.directions.map(fn(v) { v.map(fn(x) { x.copy() }) }),
    custom: self.custom.copy(),
  }
}

///|
fn clean_text(text : String) -> Bool {
  for c in text.iter() {
    if c.to_int() < 32 || c.to_int() > 126 {
      return false
    }
  }
  true
}

///|
fn finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn space_size(space : String) -> Int32 raise NrrdError {
  match space.to_lower() {
    "right-anterior-superior"
    | "ras"
    | "left-anterior-superior"
    | "las"
    | "left-posterior-superior"
    | "lps"
    | "scanner-xyz"
    | "3d-right-handed"
    | "3d-left-handed" => 3
    "right-anterior-superior-time"
    | "rast"
    | "left-anterior-superior-time"
    | "last"
    | "left-posterior-superior-time"
    | "lpst"
    | "scanner-xyz-time"
    | "3d-right-handed-time"
    | "3d-left-handed-time" => 4
    _ => raise Unsupported("space: " + space)
  }
}

///|
fn Metadata::spatial_dimension(self : Metadata) -> Int32? raise NrrdError {
  match (self.space, self.space_dimension) {
    (Some(_), Some(_)) =>
      raise Invalid("space and space dimension are mutually exclusive")
    (Some(s), None) => Some(space_size(s))
    (None, d) => d
  }
}

///|
fn Metadata::validate(self : Metadata, shape : Shape) -> Unit raise NrrdError {
  let dim = shape.dimension()
  for
    length in [
      self.labels.length(),
      self.units.length(),
      self.kinds.length(),
      self.spacings.length(),
      self.directions.length(),
    ] {
    if length != 0 && length != dim {
      raise Invalid("metadata axis count")
    }
  }
  if self.content is Some(s) {
    if !clean_text(s) {
      raise Invalid("content contains controls")
    }
  }
  for text in self.labels + self.units {
    if !clean_text(text) {
      raise Invalid("axis text contains controls")
    }
  }
  for kind in self.kinds {
    let known = match kind {
      "domain"
      | "space"
      | "time"
      | "list"
      | "point"
      | "vector"
      | "covariant-vector"
      | "normal"
      | "stub"
      | "scalar"
      | "complex"
      | "2-vector"
      | "3-color"
      | "RGB-color"
      | "HSV-color"
      | "XYZ-color"
      | "4-color"
      | "RGBA-color"
      | "3-vector"
      | "3-gradient"
      | "3-normal"
      | "4-vector"
      | "quaternion"
      | "2D-symmetric-matrix"
      | "2D-masked-symmetric-matrix"
      | "2D-matrix"
      | "2D-masked-matrix"
      | "3D-symmetric-matrix"
      | "3D-masked-symmetric-matrix"
      | "3D-matrix"
      | "3D-masked-matrix"
      | "???"
      | "none" => true
      _ => false
    }
    if !known {
      raise Unsupported("axis kind: " + kind)
    }
  }
  for spacing in self.spacings {
    if !spacing.is_nan() && (!finite(spacing) || spacing <= 0.0) {
      raise Invalid("spacing must be positive or NaN")
    }
  }
  for axis, kind in self.kinds {
    let fixed = match kind {
      "stub" | "scalar" => Some(1)
      "complex" | "2-vector" => Some(2)
      "3-color"
      | "RGB-color"
      | "HSV-color"
      | "XYZ-color"
      | "3-vector"
      | "3-gradient"
      | "3-normal"
      | "2D-symmetric-matrix" => Some(3)
      "4-color"
      | "RGBA-color"
      | "4-vector"
      | "quaternion"
      | "2D-masked-symmetric-matrix"
      | "2D-matrix" => Some(4)
      "2D-masked-matrix" => Some(5)
      "3D-symmetric-matrix" => Some(6)
      "3D-masked-symmetric-matrix" => Some(7)
      "3D-matrix" => Some(9)
      "3D-masked-matrix" => Some(10)
      _ => None
    }
    if fixed is Some(n) {
      if shape.axes[axis] != n {
        raise Invalid("fixed-size axis kind mismatch")
      }
    }
  }
  let sd = self.spatial_dimension()
  if sd is Some(d) {
    if d < 1 || d > 8 {
      raise Invalid("space dimension must be 1..8")
    }
  }
  if (self.origin != None || !self.directions.is_empty()) && sd == None {
    raise Invalid("spatial vectors require space or space dimension")
  }
  let vectors : Array[Array[Double]] = []
  if self.origin is Some(v) {
    vectors.push(v)
  }
  for v in self.directions {
    if v is Some(a) {
      vectors.push(a)
    }
  }
  for v in vectors {
    if Some(v.length()) != sd {
      raise Invalid("spatial vector dimension")
    }
    for x in v {
      if !finite(x) {
        raise Invalid("non-finite spatial vector")
      }
    }
  }
  if !self.directions.is_empty() && !self.spacings.is_empty() {
    raise Invalid("spacings and space directions cannot coexist")
  }
  for key, value in self.custom {
    if key.is_empty() ||
      !clean_text(key) ||
      key.contains(":") ||
      key.has_prefix("#") ||
      key.trim().to_owned() != key ||
      !clean_text(value) {
      raise Invalid("custom key/value cannot be safely serialized")
    }
  }
}