///|
/// A biological span with optional remote accession metadata.
pub struct LocationSummary {
  raw : String
  minimum : Int
  maximum : Int
  length : Int
  segment_count : Int
  reverse_segments : Int
  remote_segments : Int
  fuzzy : Bool
  between : Bool
} derive(Debug, Eq)

///|
pub fn summarize_location(location : Location) -> LocationSummary {
  let mut reverse = 0
  let mut remote = 0
  for segment in location.segments {
    if segment.reverse {
      reverse += 1
    }
    if segment.remote is Some(_) {
      remote += 1
    }
  }
  {
    raw: location.raw,
    minimum: location_minimum(location),
    maximum: location_span(location),
    length: location_length(location),
    segment_count: location.segments.length(),
    reverse_segments: reverse,
    remote_segments: remote,
    fuzzy: location_is_fuzzy(location),
    between: location.ranges.any(fn(range) { range.between }),
  }
}

///|
pub fn LocationSummary::to_json_string(self : LocationSummary) -> String {
  "{\"raw\":" +
  json_string(self.raw) +
  ",\"minimum\":" +
  self.minimum.to_string() +
  ",\"maximum\":" +
  self.maximum.to_string() +
  ",\"length\":" +
  self.length.to_string() +
  ",\"segment_count\":" +
  self.segment_count.to_string() +
  ",\"reverse_segments\":" +
  self.reverse_segments.to_string() +
  ",\"remote_segments\":" +
  self.remote_segments.to_string() +
  ",\"fuzzy\":" +
  self.fuzzy.to_string() +
  ",\"between\":" +
  self.between.to_string() +
  "}"
}

///|
pub fn location_segments(location : Location) -> Array[Range] {
  location.segments.map(fn(segment) { segment.range })
}

///|
pub fn location_forward_coordinates(location : Location) -> Array[Int] {
  let result : Array[Int] = []
  for segment in location.segments {
    for coordinate in segment.range.start..<=segment.range.end {
      result.push(coordinate)
    }
  }
  result
}

///|
pub fn location_unique_coordinates(location : Location) -> Array[Int] {
  let result : Array[Int] = []
  for coordinate in location_coordinates(location) {
    if !result.contains(coordinate) {
      result.push(coordinate)
    }
  }
  result
}

///|
pub fn location_has_overlap(left : Location, right : Location) -> Bool {
  left.ranges.any(fn(a) {
    right.ranges.any(fn(b) { a.start <= b.end && b.start <= a.end })
  })
}

///|
pub fn location_distance(left : Location, right : Location) -> Int? {
  if location_has_overlap(left, right) {
    Some(0)
  } else {
    let left_end = location_span(left)
    let right_end = location_span(right)
    let left_start = location_minimum(left)
    let right_start = location_minimum(right)
    if left_end < right_start {
      Some(right_start - left_end)
    } else if right_end < left_start {
      Some(left_start - right_end)
    } else {
      None
    }
  }
}

///|
pub fn location_is_single_range(location : Location) -> Bool {
  location.segments.length() == 1
}

///|
pub fn location_is_joined(location : Location) -> Bool {
  location.joined || location.segments.length() > 1
}

///|
pub fn location_is_ordered(location : Location) -> Bool {
  location.ordered
}

///|
pub fn location_is_complemented(location : Location) -> Bool {
  location.complement || location.segments.any(fn(segment) { segment.reverse })
}

///|
pub fn location_remote_accessions(location : Location) -> Array[String] {
  let result : Array[String] = []
  for segment in location.segments {
    match segment.remote {
      Some(accession) if !result.contains(accession) => result.push(accession)
      _ => ()
    }
  }
  result
}

///|
pub fn location_partial_ranges(location : Location) -> Array[Range] {
  location.ranges.filter(fn(range) { range.partial_start || range.partial_end })
}

///|
pub fn location_between_ranges(location : Location) -> Array[Range] {
  location.ranges.filter(fn(range) { range.between })
}

///|
pub fn location_range_text(range : Range) -> String {
  let start = (if range.partial_start { "<" } else { "" }) +
    range.start.to_string()
  let end = (if range.partial_end { ">" } else { "" }) + range.end.to_string()
  if range.between {
    start + "^" + end
  } else if range.start == range.end {
    start
  } else {
    start + ".." + end
  }
}

///|
pub fn location_ranges_text(location : Location) -> Array[String] {
  location.ranges.map(location_range_text)
}

///|
pub fn location_summary_text(location : Location) -> String {
  let summary = summarize_location(location)
  "location: " +
  summary.raw +
  "\n" +
  "span: " +
  summary.minimum.to_string() +
  ".." +
  summary.maximum.to_string() +
  "\n" +
  "length: " +
  summary.length.to_string() +
  "\n" +
  "segments: " +
  summary.segment_count.to_string() +
  "\n" +
  "reverse: " +
  summary.reverse_segments.to_string() +
  "\n" +
  "remote: " +
  summary.remote_segments.to_string() +
  "\n" +
  "fuzzy: " +
  summary.fuzzy.to_string() +
  "\n"
}

///|
pub fn features_with_location_property(
  features : Array[Feature],
  include_remote : Bool,
  include_fuzzy : Bool,
) -> Array[Feature] {
  features.filter(fn(feature) {
    (include_remote && location_is_remote(feature.location)) ||
    (include_fuzzy && location_is_fuzzy(feature.location))
  })
}

///|
pub fn features_with_strand(
  features : Array[Feature],
  reverse : Bool,
) -> Array[Feature] {
  features.filter(fn(feature) {
    location_is_complemented(feature.location) == reverse
  })
}

///|
pub fn feature_span(feature : Feature) -> Int {
  location_span(feature.location)
}

///|
pub fn feature_length(feature : Feature) -> Int {
  location_length(feature.location)
}

///|
pub fn feature_start(feature : Feature) -> Int {
  location_minimum(feature.location)
}

///|
pub fn feature_end(feature : Feature) -> Int {
  location_span(feature.location)
}

///|
pub fn feature_is_empty(feature : Feature) -> Bool {
  feature.location.segments.length() == 0
}

///|
pub fn feature_is_cds(feature : Feature) -> Bool {
  feature.key == "CDS"
}

///|
pub fn feature_is_gene(feature : Feature) -> Bool {
  feature.key == "gene"
}

///|
pub fn feature_is_source(feature : Feature) -> Bool {
  feature.key == "source"
}

///|
pub fn feature_has_translation(feature : Feature) -> Bool {
  feature.qualifier("translation") is Some(_)
}

///|
pub fn feature_gene_name(feature : Feature) -> String? {
  feature.qualifier("gene")
}

///|
pub fn feature_product_name(feature : Feature) -> String? {
  feature.qualifier("product")
}

///|
pub fn feature_note_values(feature : Feature) -> Array[String] {
  feature.qualifiers.filter_map(fn(qualifier) {
    if qualifier.name == "note" {
      qualifier.value
    } else {
      None
    }
  })
}

///|
pub fn feature_qualifier_names(feature : Feature) -> Array[String] {
  feature.qualifiers.map(fn(qualifier) { qualifier.name })
}

///|
pub fn feature_qualifier_count(feature : Feature) -> Int {
  feature.qualifiers.length()
}

///|
pub fn feature_summary_text(feature : Feature) -> String {
  let gene = match feature_gene_name(feature) {
    Some(name) => " gene=" + name
    None => ""
  }
  feature.key + " " + render_location(feature.location) + gene
}

///|
pub fn record_location_summaries(record : Record) -> Array[LocationSummary] {
  record.features.map(fn(feature) { summarize_location(feature.location) })
}

///|
pub fn record_location_json(record : Record) -> String {
  array_json(
    record_location_summaries(record).map(fn(summary) {
      summary.to_json_string()
    }),
  )
}

///|
pub fn record_span(record : Record) -> Int {
  record.features.fold(init=0, fn(best, feature) {
    Int::max(best, feature_end(feature))
  })
}

///|
pub fn record_minimum_feature_start(record : Record) -> Int {
  let starts = record.features.map(feature_start)
  if starts.length() == 0 {
    0
  } else {
    starts.fold(init=starts[0], fn(a, b) { Int::min(a, b) })
  }
}

///|
pub fn record_maximum_feature_end(record : Record) -> Int {
  record_span(record)
}

///|
pub fn record_feature_span(record : Record) -> Int {
  let minimum = record_minimum_feature_start(record)
  if minimum == 0 {
    0
  } else {
    record_span(record) - minimum + 1
  }
}