///|
pub(all) enum GenotypeClass {
  Missing
  Reference
  Heterozygous
  HomozygousAlternate
  MultiAlternate
  Invalid
} derive(Debug, Eq)

///|
pub(all) struct GenotypeCall {
  alleles : Array[Int?]
  phased : Bool
  invalid : Bool
} derive(Debug, Eq)

///|
pub fn decode_genotype(raw : String) -> GenotypeCall {
  if raw == "" || raw == "." {
    { alleles: [None], phased: false, invalid: false }
  } else {
    let phased = raw.contains("|")
    let separator = if phased { "|" } else { "/" }
    let parts = raw
      .split(separator[:])
      .map(fn(item) { item.to_owned() })
      .to_array()
    let alleles : Array[Int?] = []
    let mut invalid = false
    for part in parts {
      if part == "." || part == "" {
        alleles.push(None)
      } else {
        match parse_genotype_int(part) {
          Some(index) if index >= 0 => alleles.push(Some(index))
          _ => {
            alleles.push(None)
            invalid = true
          }
        }
      }
    }
    { alleles, phased, invalid }
  }
}

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

///|
pub fn genotype_is_called(call : GenotypeCall) -> Bool {
  !call.invalid &&
  call.alleles.length() > 0 &&
  call.alleles.all(fn(allele) { allele is Some(_) })
}

///|
pub fn genotype_alt_count(call : GenotypeCall) -> Int {
  let mut count = 0
  for allele in call.alleles {
    match allele {
      Some(index) if index > 0 => count = count + 1
      _ => ()
    }
  }
  count
}

///|
pub fn genotype_dosage(call : GenotypeCall) -> Int? {
  if genotype_is_called(call) {
    Some(genotype_alt_count(call))
  } else {
    None
  }
}

///|
pub fn genotype_class(call : GenotypeCall) -> GenotypeClass {
  if call.invalid {
    GenotypeClass::Invalid
  } else if !genotype_is_called(call) {
    GenotypeClass::Missing
  } else if genotype_alt_count(call) == 0 {
    GenotypeClass::Reference
  } else if call.alleles.length() == 2 && genotype_alt_count(call) == 1 {
    GenotypeClass::Heterozygous
  } else if call.alleles.length() == 2 &&
    genotype_alt_count(call) == 2 &&
    call.alleles[0] == call.alleles[1] {
    GenotypeClass::HomozygousAlternate
  } else {
    GenotypeClass::MultiAlternate
  }
}

///|
pub fn format_genotype(call : GenotypeCall) -> String {
  let separator = if call.phased { "|" } else { "/" }
  call.alleles
  .map(fn(allele) {
    match allele {
      Some(index) => index.to_string()
      None => "."
    }
  })
  .join(separator)
}

///|
pub fn genotype_alt_indexes(call : GenotypeCall) -> Array[Int] {
  let indexes : Array[Int] = []
  for allele in call.alleles {
    match allele {
      Some(index) if index > 0 => indexes.push(index)
      _ => ()
    }
  }
  indexes
}

///|
pub fn genotype_has_allele(call : GenotypeCall, wanted : Int) -> Bool {
  for allele in call.alleles {
    if allele == Some(wanted) {
      return true
    }
  }
  false
}

///|
pub fn genotype_is_hom_ref(call : GenotypeCall) -> Bool {
  genotype_class(call) == GenotypeClass::Reference
}

///|
pub fn genotype_is_het(call : GenotypeCall) -> Bool {
  genotype_class(call) == GenotypeClass::Heterozygous
}

///|
pub fn genotype_is_hom_alt(call : GenotypeCall) -> Bool {
  genotype_class(call) == GenotypeClass::HomozygousAlternate
}

///|
pub fn genotype_is_missing(call : GenotypeCall) -> Bool {
  genotype_class(call) == GenotypeClass::Missing
}