///|
/// Render a record as a canonical GenBank flat-file document.
pub fn Record::to_genbank_string(
  self : Record,
  line_width? : Int = 80,
) -> String {
  let width = if line_width < 20 { 80 } else { line_width }
  let out = StringBuilder()
  out.write_string(self.locus.to_genbank_line())
  out.write_char('\n')
  write_wrapped_field(out, "DEFINITION", self.definition, width)
  if self.accession != "" {
    write_wrapped_field(out, "ACCESSION", self.accession, width)
  }
  if self.version != "" {
    write_wrapped_field(out, "VERSION", self.version, width)
  }
  if self.keywords != "" {
    write_wrapped_field(out, "KEYWORDS", self.keywords, width)
  }
  if self.source != "" {
    write_wrapped_field(out, "SOURCE", self.source, width)
  }
  out.write_string("FEATURES             Location/Qualifiers\n")
  for feature in self.features {
    out.write_string(feature.to_genbank_head())
    out.write_char('\n')
    for qualifier in feature.qualifiers {
      for line in qualifier.to_genbank_lines(line_width=width) {
        out.write_string(line)
        out.write_char('\n')
      }
    }
  }
  out.write_string("ORIGIN\n")
  out.write_string(format_origin(self.origin, width))
  out.write_string("//\n")
  out.to_string()
}

///|
pub fn Locus::to_genbank_line(self : Locus) -> String {
  let fields = [
    self.name,
    self.length.to_string(),
    "bp",
    self.molecule_type,
    self.topology,
    self.division,
    self.date,
  ]
  "LOCUS       " + fields.filter(fn(item) { item != "" }).join(" ")
}

///|
pub fn Feature::to_genbank_head(self : Feature) -> String {
  "     " + pad_right(self.key, 16) + self.location.raw
}

///|
pub fn Qualifier::to_genbank_lines(
  self : Qualifier,
  line_width? : Int = 80,
) -> Array[String] {
  let lines : Array[String] = []
  let prefix = "                     /" + self.name
  match self.value {
    None => lines.push(prefix)
    Some(value) => {
      let encoded = if value.contains("\"") {
        "\"" + value.replace_all(old="\"", new="\\\"") + "\""
      } else {
        "\"" + value + "\""
      }
      let first = prefix + "="
      let available = Int::max(8, line_width - first.length())
      let chunks = wrap_text(encoded, available)
      if chunks.length() == 0 {
        lines.push(first)
      } else {
        lines.push(first + chunks[0])
        for chunk in chunks.view(start=1) {
          lines.push("                     " + chunk)
        }
      }
    }
  }
  lines
}

///|
fn write_wrapped_field(
  out : StringBuilder,
  name : String,
  value : String,
  line_width : Int,
) -> Unit {
  let prefix = pad_right(name, 12)
  let available = Int::max(8, line_width - prefix.length())
  let chunks = wrap_text(value, available)
  if chunks.length() == 0 {
    out.write_string(prefix)
    out.write_char('\n')
  } else {
    out.write_string(prefix + chunks[0])
    out.write_char('\n')
    for chunk in chunks.view(start=1) {
      out.write_string("            " + chunk)
      out.write_char('\n')
    }
  }
}

///|
fn format_origin(sequence : String, line_width : Int) -> String {
  let out = StringBuilder()
  let normalized = normalize_sequence(sequence)
  let bases_per_line = Int::max(10, (line_width - 10) / 11 * 10)
  let mut offset = 0
  while offset < normalized.length() {
    let end = Int::min(offset + bases_per_line, normalized.length())
    let piece = normalized.unsafe_substring(start=offset, end~)
    out.write_string(pad_left((offset + 1).to_string(), 9))
    let mut group_start = 0
    while group_start < piece.length() {
      let group_end = Int::min(group_start + 10, piece.length())
      if group_start > 0 {
        out.write_char(' ')
      }
      out.write_string(piece.unsafe_substring(start=group_start, end=group_end))
      group_start = group_end
    }
    out.write_char('\n')
    offset = end
  }
  out.to_string()
}

///|
fn pad_right(text : String, width : Int) -> String {
  let out = StringBuilder()
  out.write_string(text)
  let mut count = text.length()
  while count < width {
    out.write_char(' ')
    count += 1
  }
  out.to_string()
}

///|
fn pad_left(text : String, width : Int) -> String {
  let out = StringBuilder()
  let mut count = text.length()
  while count < width {
    out.write_char(' ')
    count += 1
  }
  out.write_string(text)
  out.to_string()
}

///|
fn wrap_text(text : String, width : Int) -> Array[String] {
  let chunks : Array[String] = []
  if text == "" {
    return chunks
  }
  let safe_width = Int::max(1, width)
  let mut start = 0
  while start < text.length() {
    let end = Int::min(start + safe_width, text.length())
    chunks.push(text.unsafe_substring(start~, end~))
    start = end
  }
  chunks
}

///|
/// Normalize line endings and ensure a final GenBank terminator.
pub fn normalize_genbank_text(text : String) -> String {
  let normalized = text.replace_all(old="\r\n", new="\n")
  if normalized.trim().to_owned().has_suffix("//") {
    if normalized.has_suffix("\n") {
      normalized
    } else {
      normalized + "\n"
    }
  } else {
    normalized + "//\n"
  }
}

///|
pub fn genbank_round_trip(text : String) -> String raise GenBankError {
  parse_record(normalize_genbank_text(text)).to_genbank_string()
}

///|
pub fn record_text_lines(record : Record) -> Array[String] {
  record
  .to_genbank_string()
  .split("\n")
  .map(fn(line) { line.to_owned() })
  .to_array()
}

///|
pub fn record_text_length(record : Record) -> Int {
  record.to_genbank_string().length()
}

///|
pub fn record_has_terminator(record : Record) -> Bool {
  record.to_genbank_string().has_suffix("//\n")
}