///|
fn gff3_escape_value(value : String) -> String {
  value
  .replace(old="%", new="%25")
  .replace(old=";", new="%3B")
  .replace(old="=", new="%3D")
  .replace(old=" ", new="%20")
}

///|
fn gtf_escape_value(value : String) -> String {
  value.replace(old="\\", new="\\\\").replace(old="\"", new="\\\"")
}

///|
fn option_to_field(value : String?) -> String {
  match value {
    Some(text) => text
    None => "."
  }
}

///|
fn gff3_attribute_text(attributes : Array[Attribute]) -> String {
  if attributes.length() == 0 {
    "."
  } else {
    let parts : Array[String] = []
    for attr in attributes {
      if attr.value.is_empty() {
        parts.push(gff3_escape_value(attr.key))
      } else {
        parts.push(
          "\{gff3_escape_value(attr.key)}=\{gff3_escape_value(attr.value)}",
        )
      }
    }
    parts.join(";")
  }
}

///|
fn gtf_attribute_text(attributes : Array[Attribute]) -> String {
  if attributes.length() == 0 {
    "."
  } else {
    let parts : Array[String] = []
    for attr in attributes {
      parts.push("\{attr.key} \"\{gtf_escape_value(attr.value)}\"")
    }
    parts.join("; ") + ";"
  }
}

///|
pub fn Feature::to_gff3_line(self : Feature) -> String {
  [
    self.seqid,
    self.source,
    self.feature_type,
    "\{self.start}",
    "\{self.end}",
    option_to_field(self.score),
    self.strand.to_string(),
    option_to_field(self.phase),
    gff3_attribute_text(self.attributes),
  ].join("\t")
}

///|
pub fn Feature::to_gtf_line(self : Feature) -> String {
  [
    self.seqid,
    self.source,
    self.feature_type,
    "\{self.start}",
    "\{self.end}",
    option_to_field(self.score),
    self.strand.to_string(),
    option_to_field(self.phase),
    gtf_attribute_text(self.attributes),
  ].join("\t")
}

///|
pub fn Annotation::to_gff3(self : Annotation) -> String {
  let lines : Array[String] = ["##gff-version 3"]
  for feature in self.features {
    lines.push(feature.to_gff3_line())
  }
  lines.join("\n") + "\n"
}

///|
pub fn Annotation::to_gtf(self : Annotation) -> String {
  let lines : Array[String] = []
  for feature in self.features {
    lines.push(feature.to_gtf_line())
  }
  if lines.length() == 0 {
    ""
  } else {
    lines.join("\n") + "\n"
  }
}

///|
pub fn Feature::to_debug_tsv(self : Feature) -> String {
  [
    self.seqid,
    self.source,
    self.feature_type,
    "\{self.start}",
    "\{self.end}",
    self.strand.to_string(),
    "\{self.attributes.length()}",
  ].join("\t")
}

///|
pub fn Annotation::feature_table_tsv(self : Annotation) -> String {
  let lines : Array[String] = [
    "seqid\tsource\tfeature_type\tstart\tend\tstrand\tattribute_count",
  ]
  for feature in self.features {
    lines.push(feature.to_debug_tsv())
  }
  lines.join("\n") + "\n"
}

///|
pub fn Annotation::gene_table_tsv(self : Annotation) -> String {
  let lines : Array[String] = [
    "gene_id\tgene_name\tseqid\tstart\tend\tstrand\ttranscripts",
  ]
  for gene in self.to_gene_models() {
    lines.push(
      [
        gene.id,
        gene.name.unwrap_or(""),
        gene.feature.seqid,
        "\{gene.feature.start}",
        "\{gene.feature.end}",
        gene.feature.strand.to_string(),
        "\{gene.transcripts.length()}",
      ].join("\t"),
    )
  }
  lines.join("\n") + "\n"
}

///|
pub fn Annotation::transcript_table_tsv(self : Annotation) -> String {
  let lines : Array[String] = [
    "gene_id\ttranscript_id\ttranscript_name\tseqid\tstart\tend\tstrand\texons\tcds",
  ]
  for gene in self.to_gene_models() {
    for tx in gene.transcripts {
      lines.push(
        [
          gene.id,
          tx.id,
          tx.name.unwrap_or(""),
          tx.feature.seqid,
          "\{tx.feature.start}",
          "\{tx.feature.end}",
          tx.feature.strand.to_string(),
          "\{tx.exons.length()}",
          "\{tx.cds.length()}",
        ].join("\t"),
      )
    }
  }
  lines.join("\n") + "\n"
}