///|
/// A compact, deterministic annotation summary for dashboards and reports.
pub struct AnnotationSummary {
  feature_count : Int
  cds_count : Int
  gene_count : Int
  source_count : Int
  base_count : Int
  gc_percent : Double
  feature_keys : Array[String]
  qualifier_names : Array[String]
} derive(Debug, Eq)

///|
pub fn location_span(location : Location) -> Int {
  let mut maximum = 0
  for range in location.ranges {
    if range.end > maximum {
      maximum = range.end
    }
  }
  maximum
}

///|
pub fn location_minimum(location : Location) -> Int {
  let mut minimum = 2147483647
  for range in location.ranges {
    if range.start < minimum {
      minimum = range.start
    }
  }
  if minimum == 2147483647 {
    0
  } else {
    minimum
  }
}

///|
pub fn location_length(location : Location) -> Int {
  location.ranges.fold(init=0, fn(total, range) {
    total + (if range.between { 0 } else { range.end - range.start + 1 })
  })
}

///|
pub fn location_contains(location : Location, position : Int) -> Bool {
  location.ranges.any(fn(range) {
    range.start <= position && position <= range.end
  })
}

///|
/// Emit each coordinate in biological order, reversing complemented segments.
pub fn location_coordinates(location : Location) -> Array[Int] {
  let coordinates : Array[Int] = []
  for segment in location.segments {
    if segment.reverse {
      let mut coordinate = segment.range.end
      while coordinate >= segment.range.start {
        coordinates.push(coordinate)
        coordinate -= 1
      }
    } else {
      for coordinate in segment.range.start..<=segment.range.end {
        coordinates.push(coordinate)
      }
    }
  }
  coordinates
}

///|
pub fn location_is_fuzzy(location : Location) -> Bool {
  location.ranges.any(fn(range) { range.partial_start || range.partial_end })
}

///|
pub fn location_is_remote(location : Location) -> Bool {
  location.segments.any(fn(segment) { segment.remote is Some(_) })
}

///|
pub fn render_location(location : Location) -> String {
  location.raw
}

///|
pub fn feature_overlaps_any(
  features : Array[Feature],
  start : Int,
  end : Int,
) -> Array[Feature] {
  features.filter(fn(feature) { feature.overlaps(start, end) })
}

///|
pub fn feature_qualifier_values(
  features : Array[Feature],
  qualifier_name : String,
) -> Array[String] {
  let values : Array[String] = []
  for feature in features {
    match feature.qualifier(qualifier_name) {
      Some(value) => values.push(value)
      None => ()
    }
  }
  values
}

///|
pub fn annotation_summary(record : Record) -> AnnotationSummary {
  let keys : Array[String] = []
  let qualifier_names : Array[String] = []
  let mut cds_count = 0
  let mut gene_count = 0
  let mut source_count = 0
  for feature in record.features {
    keys.push(feature.key)
    if feature.key == "CDS" {
      cds_count += 1
    }
    if feature.key == "gene" {
      gene_count += 1
    }
    if feature.key == "source" {
      source_count += 1
    }
    for qualifier in feature.qualifiers {
      if !qualifier_names.contains(qualifier.name) {
        qualifier_names.push(qualifier.name)
      }
    }
  }
  let stats = record.sequence_stats()
  {
    feature_count: record.features.length(),
    cds_count,
    gene_count,
    source_count,
    base_count: stats.length,
    gc_percent: stats.gc_percent,
    feature_keys: keys,
    qualifier_names,
  }
}

///|
pub fn AnnotationSummary::to_json_string(self : AnnotationSummary) -> String {
  "{\"feature_count\":" +
  self.feature_count.to_string() +
  ",\"cds_count\":" +
  self.cds_count.to_string() +
  ",\"gene_count\":" +
  self.gene_count.to_string() +
  ",\"source_count\":" +
  self.source_count.to_string() +
  ",\"base_count\":" +
  self.base_count.to_string() +
  ",\"gc_percent\":" +
  self.gc_percent.to_string() +
  ",\"feature_keys\":" +
  array_json(self.feature_keys.map(fn(item) { json_string(item) })) +
  ",\"qualifier_names\":" +
  array_json(self.qualifier_names.map(fn(item) { json_string(item) })) +
  "}"
}