///|
/// An immutable-style feature index built for repeated annotation queries.
pub struct FeatureIndex {
  record : Record
  entries : Array[FeatureIndexEntry]
} derive(Debug, Eq)

///|
struct FeatureIndexEntry {
  position : Int
  key : String
  start : Int
  end : Int
  qualifiers : Array[Qualifier]
} derive(Debug, Eq)

///|
pub fn build_feature_index(record : Record) -> FeatureIndex {
  let entries : Array[FeatureIndexEntry] = []
  for position, feature in record.features {
    entries.push({
      position,
      key: feature.key,
      start: location_minimum(feature.location),
      end: location_span(feature.location),
      qualifiers: feature.qualifiers,
    })
  }
  { record, entries }
}

///|
pub fn FeatureIndex::count(self : FeatureIndex) -> Int {
  self.entries.length()
}

///|
pub fn FeatureIndex::all(self : FeatureIndex) -> Array[Feature] {
  self.record.features
}

///|
pub fn FeatureIndex::by_key(
  self : FeatureIndex,
  key : String,
) -> Array[Feature] {
  let result : Array[Feature] = []
  for entry in self.entries {
    if entry.key == key {
      result.push(self.record.features[entry.position])
    }
  }
  result
}

///|
pub fn FeatureIndex::by_keys(
  self : FeatureIndex,
  keys : Array[String],
) -> Array[Feature] {
  let result : Array[Feature] = []
  for entry in self.entries {
    if keys.contains(entry.key) {
      result.push(self.record.features[entry.position])
    }
  }
  result
}

///|
pub fn FeatureIndex::by_qualifier(
  self : FeatureIndex,
  name : String,
  value : String,
) -> Array[Feature] {
  let result : Array[Feature] = []
  for entry in self.entries {
    for qualifier in entry.qualifiers {
      match qualifier.value {
        Some(actual) if qualifier.name == name && actual == value =>
          result.push(self.record.features[entry.position])
        _ => ()
      }
    }
  }
  result
}

///|
pub fn FeatureIndex::with_qualifier(
  self : FeatureIndex,
  name : String,
) -> Array[Feature] {
  let result : Array[Feature] = []
  for entry in self.entries {
    if entry.qualifiers.any(fn(item) { item.name == name }) {
      result.push(self.record.features[entry.position])
    }
  }
  result
}

///|
pub fn FeatureIndex::overlapping(
  self : FeatureIndex,
  start : Int,
  end : Int,
) -> Array[Feature] {
  let result : Array[Feature] = []
  if start > end {
    return result
  }
  for entry in self.entries {
    if entry.start <= end && start <= entry.end {
      result.push(self.record.features[entry.position])
    }
  }
  result
}

///|
pub fn FeatureIndex::contained_in(
  self : FeatureIndex,
  start : Int,
  end : Int,
) -> Array[Feature] {
  let result : Array[Feature] = []
  if start > end {
    return result
  }
  for entry in self.entries {
    if start <= entry.start && entry.end <= end {
      result.push(self.record.features[entry.position])
    }
  }
  result
}

///|
pub fn FeatureIndex::at(self : FeatureIndex, position : Int) -> Array[Feature] {
  self.overlapping(position, position)
}

///|
pub fn FeatureIndex::keys(self : FeatureIndex) -> Array[String] {
  let result : Array[String] = []
  for entry in self.entries {
    if !result.contains(entry.key) {
      result.push(entry.key)
    }
  }
  result
}

///|
pub fn FeatureIndex::positions(self : FeatureIndex, key : String) -> Array[Int] {
  self.entries
  .filter(fn(entry) { entry.key == key })
  .map(fn(entry) { entry.position })
}

///|
pub fn FeatureIndex::qualifier_names(self : FeatureIndex) -> Array[String] {
  let result : Array[String] = []
  for entry in self.entries {
    for qualifier in entry.qualifiers {
      if !result.contains(qualifier.name) {
        result.push(qualifier.name)
      }
    }
  }
  result
}

///|
pub fn FeatureIndex::summary_json(self : FeatureIndex) -> String {
  let out = StringBuilder()
  out.write_string("{\"count\":")
  out.write_string(self.count().to_string())
  out.write_string(",\"keys\":")
  out.write_string(array_json(self.keys().map(fn(key) { json_string(key) })))
  out.write_string(",\"qualifiers\":")
  out.write_string(
    array_json(self.qualifier_names().map(fn(name) { json_string(name) })),
  )
  out.write_string("}")
  out.to_string()
}

///|
pub fn feature_key_counts(record : Record) -> Array[(String, Int)] {
  let result : Array[(String, Int)] = []
  for feature in record.features {
    match result.search_by(fn(pair) { pair.0 == feature.key }) {
      Some(index) => {
        let current = result[index]
        result[index] = (current.0, current.1 + 1)
      }
      None => result.push((feature.key, 1))
    }
  }
  result
}

///|
pub fn feature_qualifier_counts(record : Record) -> Array[(String, Int)] {
  let result : Array[(String, Int)] = []
  for feature in record.features {
    for qualifier in feature.qualifiers {
      match result.search_by(fn(pair) { pair.0 == qualifier.name }) {
        Some(index) => {
          let current = result[index]
          result[index] = (current.0, current.1 + 1)
        }
        None => result.push((qualifier.name, 1))
      }
    }
  }
  result
}

///|
pub fn feature_density(record : Record) -> Double {
  let length = record.sequence_stats().length
  if length == 0 {
    0.0
  } else {
    record.features.length().to_double() / length.to_double()
  }
}

///|
pub fn feature_coverage(record : Record) -> Int {
  let covered : Array[Int] = []
  for feature in record.features {
    for position in location_coordinates(feature.location) {
      if !covered.contains(position) {
        covered.push(position)
      }
    }
  }
  covered.length()
}

///|
pub fn feature_coverage_percent(record : Record) -> Double {
  let length = record.sequence_stats().length
  if length == 0 {
    0.0
  } else {
    feature_coverage(record).to_double() * 100.0 / length.to_double()
  }
}

///|
pub fn source_feature(record : Record) -> Feature? {
  match record.features.search_by(fn(feature) { feature.key == "source" }) {
    Some(index) => Some(record.features[index])
    None => None
  }
}

///|
pub fn first_feature_by_key(record : Record, key : String) -> Feature? {
  match record.features.search_by(fn(feature) { feature.key == key }) {
    Some(index) => Some(record.features[index])
    None => None
  }
}

///|
pub fn feature_has_remote_location(feature : Feature) -> Bool {
  location_is_remote(feature.location)
}

///|
pub fn feature_has_fuzzy_location(feature : Feature) -> Bool {
  location_is_fuzzy(feature.location)
}