///|
pub fn validate_document(
  document : VcfDocument,
  options~ : ValidationOptions,
) -> ValidationReport {
  let issues : Array[Diagnostic] = []
  validate_header(document.header, options~, issues)
  for index, record in document.records {
    validate_record(record, document.header, options~, issues, line=index + 1)
  }
  { issues, }
}

///|
fn validate_header(
  header : VcfHeader,
  options~ : ValidationOptions,
  issues : Array[Diagnostic],
) -> Unit {
  if header.fileformat == "" {
    append_issue(
      issues,
      options~,
      code="MISSING_FILEFORMAT",
      line=0,
      column=1,
      field="fileformat",
      message="VCF header has no fileformat declaration",
    )
  }
  for kind in ["INFO", "FORMAT", "FILTER", "CONTIG"] {
    let seen : Array[String] = []
    let names = definition_names(header, kind)
    for name in names {
      if has_name(seen, name) {
        append_issue(
          issues,
          options~,
          code="DUPLICATE_\{kind}",
          line=0,
          column=1,
          field=name,
          message="duplicate {kind} definition",
        )
      } else {
        seen.push(name)
      }
    }
  }
  if header.columns.length() == 0 {
    append_issue(
      issues,
      options~,
      code="MISSING_COLUMNS",
      line=0,
      column=1,
      field="#CHROM",
      message="VCF header has no column definition",
    )
  }
}

///|
fn validate_record(
  record : VcfRecord,
  header : VcfHeader,
  options~ : ValidationOptions,
  issues : Array[Diagnostic],
  line~ : Int,
) -> Unit {
  if record.chrom == "" {
    append_issue(
      issues,
      options~,
      code="EMPTY_CHROM",
      line~,
      column=1,
      field="CHROM",
      message="CHROM must not be empty",
    )
  }
  if record.pos <= 0 {
    append_issue(
      issues,
      options~,
      code="INVALID_POS",
      line~,
      column=2,
      field="POS",
      message="POS must be positive",
    )
  }
  if record.ref_allele == "" || record.ref_allele == "." {
    append_issue(
      issues,
      options~,
      code="EMPTY_REF",
      line~,
      column=4,
      field="REF",
      message="REF must contain an allele",
    )
  }
  if record.alt_alleles.length() == 0 {
    append_issue(
      issues,
      options~,
      code="EMPTY_ALT",
      line~,
      column=5,
      field="ALT",
      message="ALT must contain at least one allele",
    )
  }
  for alt in record.alt_alleles {
    if alt == "" || alt == "." {
      append_issue(
        issues,
        options~,
        code="INVALID_ALT",
        line~,
        column=5,
        field="ALT",
        message="ALT contains an empty allele",
      )
    }
  }
  for info_pair in record.info {
    let (key, value) = info_pair
    match find_info_definition(header, key) {
      None =>
        append_issue(
          issues,
          options~,
          code="UNKNOWN_INFO",
          line~,
          column=8,
          field=key,
          message="INFO key is not declared in the header",
        )
      Some(def) =>
        match expected_value_count(def.number, record.alt_alleles.length()) {
          Some(expected) =>
            if value_count(value) != expected {
              append_issue(
                issues,
                options~,
                code="INFO_CARDINALITY",
                line~,
                column=8,
                field=key,
                message="INFO value count does not match header cardinality",
              )
            }
          None => ()
        }
    }
  }
  if record.samples.length() != header.samples.length() {
    append_issue(
      issues,
      options~,
      code="SAMPLE_COUNT_MISMATCH",
      line~,
      column=10,
      field="samples",
      message="sample count does not match header",
    )
  }
  for sample_index, sample in record.samples {
    if sample_index < header.samples.length() &&
      sample.name != header.samples[sample_index] {
      append_issue(
        issues,
        options~,
        code="SAMPLE_NAME_MISMATCH",
        line~,
        column=10 + sample_index,
        field=sample.name,
        message="sample name does not match header",
      )
    }
    if sample.values.length() != record.format_keys.length() {
      append_issue(
        issues,
        options~,
        code="FORMAT_VALUE_COUNT",
        line~,
        column=9,
        field=sample.name,
        message="sample FORMAT value count does not match FORMAT keys",
      )
    }
  }
}

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

///|
fn expected_value_count(number : Cardinality, alt_count : Int) -> Int? {
  match number {
    Cardinality::Fixed(n) => Some(n)
    Cardinality::PerAlt => Some(alt_count)
    Cardinality::PerAllele => Some(alt_count + 1)
    _ => None
  }
}

///|
fn value_count(value : VcfValue) -> Int {
  match value {
    VcfValue::List(values) => values.length()
    Missing | Integer(_) | Float(_) | String(_) => 1
    Flag => 0
  }
}