///|
pub fn parse_record(
  line : String,
  header~ : VcfHeader,
) -> VcfRecord raise ParseError {
  parse_record_at(line, header~, line_number=1)
}

///|
pub fn parse_document(text : String) -> VcfDocument raise ParseError {
  let header_lines : Array[String] = []
  let record_lines : Array[(Int, String)] = []
  let mut saw_columns = false
  for index, line_view in text.split("\n") {
    let line = line_view.to_owned()
    if line == "" {
      ()
    } else if !saw_columns {
      header_lines.push(line)
      if line.has_prefix("#CHROM") {
        saw_columns = true
      }
    } else {
      record_lines.push((index + 1, line))
    }
  }
  let header = parse_header(header_lines.join("\n") + "\n")
  let records : Array[VcfRecord] = []
  for pair in record_lines {
    let (line_number, record_line) = pair
    records.push(parse_record_at(record_line, header~, line_number~))
  }
  { header, records }
}

///|
fn parse_record_at(
  line : String,
  header~ : VcfHeader,
  line_number~ : Int,
) -> VcfRecord raise ParseError {
  let fields = @text.split_tab(line)
  guard fields.length() >= 8 else {
    raise ParseError::InvalidRecord(
      line=line_number,
      column=1,
      field="record",
      message="record must contain at least 8 fields",
    )
  }
  let chrom = fields[0]
  let pos = parse_required_int(fields[1], line_number~, column=2, field="POS")
  let ids = parse_missing_list(fields[2], ';')
  let ref_allele = fields[3]
  let alt_alleles = parse_missing_list(fields[4], ',')
  let qual = parse_optional_double(
    fields[5],
    line_number~,
    column=6,
    field="QUAL",
  )
  let filters = parse_missing_list(fields[6], ';')
  let info = parse_info(fields[7], header~, line_number~)
  let format_keys = if fields.length() > 8 && fields[8] != "." {
    @text.split_char(fields[8], ':')
  } else {
    []
  }
  let samples = parse_samples(fields, format_keys, header~, line_number~)
  {
    chrom,
    pos,
    ids,
    ref_allele,
    alt_alleles,
    qual,
    filters,
    info,
    format_keys,
    samples,
  }
}

///|
fn parse_missing_list(value : String, sep : Char) -> Array[String] {
  if value == "." || value == "" {
    []
  } else {
    @text.split_char(value, sep)
  }
}

///|
fn parse_info(
  value : String,
  header~ : VcfHeader,
  line_number~ : Int,
) -> Array[(String, VcfValue)] raise ParseError {
  if value == "." || value == "" {
    []
  } else {
    let entries : Array[(String, VcfValue)] = []
    for part in @text.split_char(value, ';') {
      if part != "" {
        match @text.split_key_value(part, '=') {
          Some((key, raw_value)) =>
            entries.push(
              (key, coerce_info_value(key, raw_value, header~, line_number~)),
            )
          None => entries.push((part, VcfValue::Flag))
        }
      }
    }
    entries
  }
}

///|
fn parse_samples(
  fields : Array[String],
  format_keys : Array[String],
  header~ : VcfHeader,
  line_number~ : Int,
) -> Array[SampleCall] raise ParseError {
  let calls : Array[SampleCall] = []
  if fields.length() <= 9 || format_keys.length() == 0 {
    calls
  } else {
    for sample_index in 0..<(fields.length() - 9) {
      let raw_values = @text.split_char(fields[9 + sample_index], ':')
      let values : Array[(String, VcfValue)] = []
      for key_index, key in format_keys {
        let raw_value = if key_index < raw_values.length() {
          raw_values[key_index]
        } else {
          "."
        }
        values.push(
          (key, coerce_format_value(key, raw_value, header~, line_number~)),
        )
      }
      let name = if sample_index < header.samples.length() {
        header.samples[sample_index]
      } else {
        "SAMPLE\{sample_index + 1}"
      }
      calls.push({ name, values })
    }
    calls
  }
}

///|
fn coerce_info_value(
  key : String,
  raw_value : String,
  header~ : VcfHeader,
  line_number~ : Int,
) -> VcfValue raise ParseError {
  match find_info_def(header, key) {
    Some(def) =>
      coerce_typed_value(
        raw_value,
        def.value_type,
        def.number,
        line_number~,
        field=key,
      )
    None =>
      coerce_typed_value(
        raw_value,
        ValueType::String,
        Cardinality::Variable,
        line_number~,
        field=key,
      )
  }
}

///|
fn coerce_format_value(
  key : String,
  raw_value : String,
  header~ : VcfHeader,
  line_number~ : Int,
) -> VcfValue raise ParseError {
  match find_format_def(header, key) {
    Some(def) =>
      coerce_typed_value(
        raw_value,
        def.value_type,
        def.number,
        line_number~,
        field=key,
      )
    None =>
      coerce_typed_value(
        raw_value,
        ValueType::String,
        Cardinality::Fixed(1),
        line_number~,
        field=key,
      )
  }
}

///|
fn coerce_typed_value(
  raw_value : String,
  value_type : ValueType,
  number : Cardinality,
  line_number~ : Int,
  field~ : String,
) -> VcfValue raise ParseError {
  if raw_value == "." {
    VcfValue::Missing
  } else if should_parse_list(number, raw_value) {
    let values : Array[VcfValue] = []
    for item in @text.split_char(raw_value, ',') {
      values.push(coerce_scalar_value(item, value_type, line_number~, field~))
    }
    VcfValue::List(values)
  } else {
    coerce_scalar_value(raw_value, value_type, line_number~, field~)
  }
}

///|
fn should_parse_list(number : Cardinality, raw_value : String) -> Bool {
  if raw_value.contains(",") {
    true
  } else {
    match number {
      Cardinality::PerAlt | PerAllele | PerGenotype | Variable => true
      _ => false
    }
  }
}

///|
fn coerce_scalar_value(
  raw_value : String,
  value_type : ValueType,
  line_number~ : Int,
  field~ : String,
) -> VcfValue raise ParseError {
  match value_type {
    ValueType::Integer =>
      VcfValue::Integer(
        parse_required_int(raw_value, line_number~, column=1, field~),
      )
    Float =>
      VcfValue::Float(
        parse_required_double(raw_value, line_number~, column=1, field~),
      )
    Flag =>
      if raw_value == "" {
        VcfValue::Flag
      } else {
        VcfValue::String(raw_value)
      }
    Character | String | Unknown(_) => VcfValue::String(raw_value)
  }
}

///|
fn find_info_def(header : VcfHeader, key : String) -> InfoDef? {
  for item in header.meta {
    match item {
      MetaLine::Info(def) => if def.id == key { return Some(def) }
      _ => ()
    }
  }
  None
}

///|
fn find_format_def(header : VcfHeader, key : String) -> FormatDef? {
  for item in header.meta {
    match item {
      MetaLine::Format(def) => if def.id == key { return Some(def) }
      _ => ()
    }
  }
  None
}

///|
fn parse_required_int(
  value : String,
  line_number~ : Int,
  column~ : Int,
  field~ : String,
) -> Int raise ParseError {
  match parse_non_negative_int(value) {
    Some(n) => n
    None =>
      raise ParseError::InvalidValue(
        line=line_number,
        column~,
        field~,
        value~,
        message="expected non-negative integer",
      )
  }
}

///|
fn parse_optional_double(
  value : String,
  line_number~ : Int,
  column~ : Int,
  field~ : String,
) -> Double? raise ParseError {
  if value == "." {
    None
  } else {
    Some(parse_required_double(value, line_number~, column~, field~))
  }
}

///|
fn parse_required_double(
  value : String,
  line_number~ : Int,
  column~ : Int,
  field~ : String,
) -> Double raise ParseError {
  @string.parse_double(value[:]) catch {
    _ =>
      raise ParseError::InvalidValue(
        line=line_number,
        column~,
        field~,
        value~,
        message="expected floating point number",
      )
  }
}