///|
/// A feature projection in BED/GFF-compatible coordinate conventions.
pub struct FeatureProjection {
  accession : String
  key : String
  start : Int
  end : Int
  strand : String
  name : String
  attributes : String
} derive(Debug, Eq)

///|
/// Convert a feature to a zero-based, half-open projection for genome tools.
pub fn project_feature(record : Record, feature : Feature) -> FeatureProjection {
  let summary = summarize_location(feature.location)
  let name = match feature.qualifier("gene") {
    Some(value) => value
    None =>
      match feature.qualifier("locus_tag") {
        Some(value) => value
        None => feature.key
      }
  }
  let attributes = feature.qualifiers
    .map(fn(item) {
      let value = match item.value {
        Some(text) => text
        None => ""
      }
      item.name + "=" + value.replace_all(old=";", new=",")
    })
    .join(";")
  {
    accession: record.accession,
    key: feature.key,
    start: Int::max(0, summary.minimum - 1),
    end: summary.maximum,
    strand: if feature.location.complement {
      "-"
    } else {
      "+"
    },
    name,
    attributes,
  }
}

///|
/// Project every local feature in source order.
pub fn project_record_features(record : Record) -> Array[FeatureProjection] {
  record.features
  .filter(fn(feature) { !location_is_remote(feature.location) })
  .map(fn(feature) { project_feature(record, feature) })
}

///|
/// Render BED6 rows; coordinates are zero-based and end-exclusive.
pub fn record_to_bed(record : Record) -> String {
  let out = StringBuilder()
  for projection in project_record_features(record) {
    out.write_string(projection.accession)
    out.write_char('\t')
    out.write_string(projection.start.to_string())
    out.write_char('\t')
    out.write_string(projection.end.to_string())
    out.write_char('\t')
    out.write_string(projection.name)
    out.write_string("\t0\t")
    out.write_string(projection.strand)
    out.write_char('\n')
  }
  out.to_string()
}

///|
/// Render GFF3 rows with escaped attributes for common downstream consumers.
pub fn record_to_gff3(record : Record) -> String {
  let out = StringBuilder()
  out.write_string("##gff-version 3\n")
  for projection in project_record_features(record) {
    out.write_string(
      projection.accession + "\tmoonbit-genbank\t" + projection.key + "\t",
    )
    out.write_string(
      (projection.start + 1).to_string() + "\t" + projection.end.to_string(),
    )
    out.write_string("\t.\t" + projection.strand + "\t.\t")
    out.write_string(
      "ID=" + projection.name + ";" + projection.attributes + "\n",
    )
  }
  out.to_string()
}

///|
/// Render an indexed FASTA header that retains accession and definition.
pub fn record_to_fasta(record : Record, line_width : Int) -> String {
  let id = if record.accession == "" {
    record.locus.name
  } else {
    record.accession
  }
  { id, description: record.definition, sequence: record.origin }.to_fasta(
    line_width,
  )
}

///|
/// Return only projections overlapping a requested one-based interval.
pub fn project_interval(
  record : Record,
  start : Int,
  end : Int,
) -> Array[FeatureProjection] {
  if start < 1 || end < start {
    return []
  }
  project_record_features(record).filter(fn(item) {
    item.start < end && start <= item.end
  })
}

///|
/// Serialize a projection for APIs that do not need the full record model.
pub fn FeatureProjection::to_json_string(self : FeatureProjection) -> String {
  "{\"accession\":" +
  json_string(self.accession) +
  ",\"key\":" +
  json_string(self.key) +
  ",\"start\":" +
  self.start.to_string() +
  ",\"end\":" +
  self.end.to_string() +
  ",\"strand\":" +
  json_string(self.strand) +
  ",\"name\":" +
  json_string(self.name) +
  ",\"attributes\":" +
  json_string(self.attributes) +
  "}"
}