///|
/// Validated header plan; payload is never opened implicitly.
pub struct Header {
  shape : Shape
  scalar : Scalar
  encoding : Encoding
  endian : Endian
  metadata : Metadata
  data_file : String?
  payload_offset : Int32
} derive(Debug)

///|
pub fn Header::shape(self : Header) -> Shape {
  self.shape
}

///|
pub fn Header::scalar(self : Header) -> Scalar {
  self.scalar
}

///|
pub fn Header::encoding(self : Header) -> Encoding {
  self.encoding
}

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

///|
pub fn Header::metadata(self : Header) -> Metadata {
  self.metadata.copy_owned()
}

///|
pub fn Header::data_file(self : Header) -> String? {
  self.data_file
}

///|
pub fn Header::payload_offset(self : Header) -> Int32 {
  self.payload_offset
}

///|
fn required(
  fields : Map[String, String],
  key : String,
) -> String raise NrrdError {
  match fields.get(key) {
    Some(s) => s
    None => raise Invalid("required header field: " + key)
  }
}

///|
fn detached_name(name : String) -> Unit raise NrrdError {
  if name.is_empty() || name == "." || name == ".." || name.to_upper() == "LIST" {
    raise Invalid("empty or relative data filename")
  }
  for c in name.iter() {
    let n = c.to_int()
    if !((n >= 48 && n <= 57) ||
      (n >= 65 && n <= 90) ||
      (n >= 97 && n <= 122) ||
      c == '_' ||
      c == '-' ||
      c == '.') {
      raise Unsupported("data file must be a single safe basename")
    }
  }
}

///|
pub fn parse_header(
  bytes : Bytes,
  limits~ : Limits = Limits::default(),
) -> Header raise NrrdError {
  let (fields, custom, payload_offset) = scan_header(bytes, limits)
  for key, _ in fields {
    match key {
      "type"
      | "dimension"
      | "sizes"
      | "encoding"
      | "endian"
      | "content"
      | "labels"
      | "units"
      | "kinds"
      | "spacings"
      | "space"
      | "space dimension"
      | "space origin"
      | "space directions"
      | "data file"
      | "line skip"
      | "byte skip" => ()
      _ => raise Unsupported("header field: " + key)
    }
  }
  let scalar = parse_scalar(required(fields, "type"))
  let dimension = integer(required(fields, "dimension"))
  let sizes = words(required(fields, "sizes")).map(s => integer(s))
  let shape = Shape::new(sizes, limits~)
  if dimension != shape.dimension() {
    raise Invalid("dimension/sizes mismatch")
  }
  ignore(shape.byte_count(scalar, limits))
  let encoding = parse_encoding(required(fields, "encoding"))
  let endian = match fields.get("endian") {
    Some("little") => Little
    Some("big") => Big
    Some(_) => raise Invalid("endian must be little or big")
    None => {
      if encoding != Ascii && scalar.width() > 1 {
        raise Invalid("multi-byte binary data requires endian")
      }
      Little
    }
  }
  for key in ["line skip", "byte skip"] {
    if fields.get(key) is Some(value) {
      if integer(value) != 0 {
        raise Unsupported("nonzero " + key)
      }
    }
  }
  let data_file = fields.get("data file")
  if data_file is Some(name) {
    detached_name(name)
  }
  let labels = match fields.get("labels") {
    Some(s) => quoted_strings(s)
    None => []
  }
  let units = match fields.get("units") {
    Some(s) => quoted_strings(s)
    None => []
  }
  let kinds = match fields.get("kinds") {
    Some(s) => words(s)
    None => []
  }
  let spacings = match fields.get("spacings") {
    Some(s) => words(s).map(v => number(v))
    None => []
  }
  let space_dimension = match fields.get("space dimension") {
    Some(s) => Some(integer(s))
    None => None
  }
  let origin = match fields.get("space origin") {
    Some(s) => Some(vector(s))
    None => None
  }
  let directions = match fields.get("space directions") {
    Some(s) => direction_list(s)
    None => []
  }
  let metadata : Metadata = {
    content: fields.get("content"),
    labels,
    units,
    kinds,
    spacings,
    space: fields.get("space"),
    space_dimension,
    origin,
    directions,
    custom,
  }
  metadata.validate(shape)
  { shape, scalar, encoding, endian, metadata, data_file, payload_offset, }
}