///|
pub fn parse_header(text : String) -> VcfHeader raise ParseError {
  let meta : Array[MetaLine] = []
  let mut fileformat = ""
  let mut columns : Array[String] = []
  let mut samples : Array[String] = []
  let mut saw_columns = false
  for index, line_view in text.split("\n") {
    let line = line_view.to_owned()
    if line == "" {
      ()
    } else if line.has_prefix("##") {
      let item = parse_meta_line(line, line_number=index + 1)
      if item is MetaLine::FileFormat(value) {
        fileformat = value
      }
      meta.push(item)
    } else if line.has_prefix("#CHROM") {
      let fields = @text.split_tab(line)
      guard fields.length() >= 8 else {
        raise ParseError::InvalidHeader(
          line=index + 1,
          column=1,
          message="column header must contain at least 8 fields",
        )
      }
      columns = fields
      samples = if fields.length() > 9 { fields[9:].to_owned() } else { [] }
      saw_columns = true
    } else {
      raise ParseError::InvalidHeader(
        line=index + 1,
        column=1,
        message="header contains a record before #CHROM",
      )
    }
  }
  guard saw_columns else {
    raise ParseError::InvalidHeader(
      line=0,
      column=1,
      message="missing #CHROM column header",
    )
  }
  { fileformat, meta, columns, samples }
}

///|
pub fn parse_meta_line(
  line : String,
  line_number~ : Int,
) -> MetaLine raise ParseError {
  guard line.has_prefix("##") else {
    raise ParseError::InvalidHeader(
      line=line_number,
      column=1,
      message="metadata line must start with ##",
    )
  }
  let body = line.unsafe_substring(start=2, end=line.length())
  guard @text.split_key_value(body, '=') is Some((key, value)) else {
    raise ParseError::InvalidHeader(
      line=line_number,
      column=3,
      message="metadata line must contain key=value",
    )
  }
  match key {
    "fileformat" => MetaLine::FileFormat(value)
    "INFO" => MetaLine::Info(parse_info_def(value, line_number~))
    "FORMAT" => MetaLine::Format(parse_format_def(value, line_number~))
    "FILTER" => MetaLine::Filter(parse_filter_def(value, line_number~))
    "contig" => MetaLine::Contig(parse_contig_def(value, line_number~))
    _ => MetaLine::Generic(key~, value~)
  }
}

///|
fn parse_info_def(
  value : String,
  line_number~ : Int,
) -> InfoDef raise ParseError {
  let attrs = parse_angle_attributes(value, line_number~)
  {
    id: require_attr(attrs, "ID", line_number~),
    number: parse_cardinality(require_attr(attrs, "Number", line_number~)),
    value_type: parse_value_type(require_attr(attrs, "Type", line_number~)),
    description: optional_attr(attrs, "Description").unwrap_or(""),
    attributes: attrs,
  }
}

///|
fn parse_format_def(
  value : String,
  line_number~ : Int,
) -> FormatDef raise ParseError {
  let attrs = parse_angle_attributes(value, line_number~)
  {
    id: require_attr(attrs, "ID", line_number~),
    number: parse_cardinality(require_attr(attrs, "Number", line_number~)),
    value_type: parse_value_type(require_attr(attrs, "Type", line_number~)),
    description: optional_attr(attrs, "Description").unwrap_or(""),
    attributes: attrs,
  }
}

///|
fn parse_filter_def(
  value : String,
  line_number~ : Int,
) -> FilterDef raise ParseError {
  let attrs = parse_angle_attributes(value, line_number~)
  {
    id: require_attr(attrs, "ID", line_number~),
    description: optional_attr(attrs, "Description").unwrap_or(""),
    attributes: attrs,
  }
}

///|
fn parse_contig_def(
  value : String,
  line_number~ : Int,
) -> ContigDef raise ParseError {
  let attrs = parse_angle_attributes(value, line_number~)
  {
    id: require_attr(attrs, "ID", line_number~),
    length: match optional_attr(attrs, "length") {
      Some(raw_length) => parse_non_negative_int(raw_length)
      None => None
    },
    attributes: attrs,
  }
}

///|
fn parse_angle_attributes(
  value : String,
  line_number~ : Int,
) -> Array[(String, String)] raise ParseError {
  guard @text.strip_angle_brackets(value) is Some(inner) else {
    raise ParseError::InvalidHeader(
      line=line_number,
      column=1,
      message="structured metadata must be wrapped in angle brackets",
    )
  }
  let attrs : Array[(String, String)] = []
  for part in @text.split_char(inner, ',') {
    guard @text.split_key_value(part, '=') is Some((key, raw_value)) else {
      raise ParseError::InvalidHeader(
        line=line_number,
        column=1,
        message="structured metadata attribute must be key=value",
      )
    }
    attrs.push((key, unquote(raw_value)))
  }
  attrs
}

///|
fn require_attr(
  attrs : Array[(String, String)],
  key : String,
  line_number~ : Int,
) -> String raise ParseError {
  match optional_attr(attrs, key) {
    Some(value) => value
    None =>
      raise ParseError::InvalidHeader(
        line=line_number,
        column=1,
        message="missing required metadata attribute \{key}",
      )
  }
}

///|
fn optional_attr(attrs : Array[(String, String)], key : String) -> String? {
  for pair in attrs {
    let (attr_key, attr_value) = pair
    if attr_key == key {
      return Some(attr_value)
    }
  }
  None
}

///|
fn parse_cardinality(value : String) -> Cardinality {
  match value {
    "A" => Cardinality::PerAlt
    "R" => Cardinality::PerAllele
    "G" => Cardinality::PerGenotype
    "." => Cardinality::Variable
    _ =>
      match parse_non_negative_int(value) {
        Some(n) => Cardinality::Fixed(n)
        None => Cardinality::Unknown(value)
      }
  }
}

///|
fn parse_value_type(value : String) -> ValueType {
  match value {
    "Integer" => ValueType::Integer
    "Float" => ValueType::Float
    "Flag" => ValueType::Flag
    "Character" => ValueType::Character
    "String" => ValueType::String
    _ => ValueType::Unknown(value)
  }
}

///|
fn parse_non_negative_int(value : String) -> Int? {
  if value == "" {
    None
  } else {
    let mut total = 0
    for ch in value {
      if ch >= '0' && ch <= '9' {
        total = total * 10 + ch.to_int() - '0'.to_int()
      } else {
        return None
      }
    }
    Some(total)
  }
}

///|
fn unquote(value : String) -> String {
  if value.length() >= 2 && value.has_prefix("\"") && value.has_suffix("\"") {
    value.unsafe_substring(start=1, end=value.length() - 1)
  } else {
    value
  }
}