///|
/// Parse one GenBank/INSDC flat-file record.
pub fn parse_record(text : String) -> Record raise GenBankError {
  let lines = text.split("\n").map(fn(line) { line.to_owned() }).to_array()
  let locus = parse_locus(require_prefixed(lines, "LOCUS"))
  let accession = parse_accession(lines)
  let version = collect_field(lines, "VERSION")
  let definition = collect_field(lines, "DEFINITION")
  let keywords = collect_field(lines, "KEYWORDS")
  let source = collect_field(lines, "SOURCE")
  let features = parse_features(lines)
  let origin = parse_origin(lines)
  { accession, version, locus, definition, keywords, source, features, origin }
}

///|
/// Parse all records in a GenBank flat-file stream separated by `//`.
pub fn parse_records(text : String) -> Array[Record] raise GenBankError {
  let records : Array[Record] = []
  let buffer = StringBuilder()
  for line in text.split("\n") {
    let owned = line.to_owned()
    if owned.trim().to_owned() == "//" {
      buffer.write_string("//\n")
      let chunk = buffer.to_string()
      if chunk.trim().to_owned() != "" {
        records.push(parse_record(chunk))
      }
      buffer.reset()
    } else {
      buffer.write_string(owned)
      buffer.write_char('\n')
    }
  }
  let tail = buffer.to_string()
  if tail.trim().to_owned() != "" {
    records.push(parse_record(tail))
  }
  records
}

///|
fn require_prefixed(
  lines : Array[String],
  prefix : String,
) -> String raise GenBankError {
  match lines.search_by(fn(line) { line.has_prefix(prefix) }) {
    Some(index) => lines[index]
    None => raise GenBankError::MissingSection(prefix)
  }
}

///|
fn parse_accession(lines : Array[String]) -> String {
  match lines.search_by(fn(line) { line.has_prefix("ACCESSION") }) {
    Some(index) =>
      match field_value(lines[index], "ACCESSION") {
        Some(value) => value
        None => ""
      }
    None => ""
  }
}

///|
fn collect_field(lines : Array[String], name : String) -> String {
  match lines.search_by(fn(line) { line.has_prefix(name) }) {
    Some(index) => {
      let first = match field_value(lines[index], name) {
        Some(value) => value
        None => ""
      }
      let parts : Array[String] = [first]
      for i = index + 1; i < lines.length(); i = i + 1 {
        let line = lines[i]
        if line.has_prefix("            ") {
          parts.push(line.trim().to_owned())
        } else {
          break
        }
      }
      parts.join(" ")
    }
    None => ""
  }
}

///|
fn parse_origin(lines : Array[String]) -> String raise GenBankError {
  match lines.search_by(fn(line) { line.has_prefix("ORIGIN") }) {
    Some(index) => {
      let pieces : Array[String] = []
      for i = index + 1; i < lines.length(); i = i + 1 {
        let line = lines[i]
        if line.has_prefix("//") {
          break
        }
        if is_origin_sequence_line(line) {
          for ch in line {
            if is_base(ch) {
              pieces.push(ch.to_string())
            }
          }
        }
      }
      pieces.join("").to_upper()
    }
    None => raise GenBankError::MissingSection("ORIGIN")
  }
}

///|
fn is_base(ch : Char) -> Bool {
  ch == 'a' ||
  ch == 'c' ||
  ch == 'g' ||
  ch == 't' ||
  ch == 'u' ||
  ch == 'n' ||
  ch == 'A' ||
  ch == 'C' ||
  ch == 'G' ||
  ch == 'T' ||
  ch == 'U' ||
  ch == 'N' ||
  ch == 'r' ||
  ch == 'y' ||
  ch == 's' ||
  ch == 'w' ||
  ch == 'k' ||
  ch == 'm' ||
  ch == 'b' ||
  ch == 'd' ||
  ch == 'h' ||
  ch == 'v' ||
  ch == 'R' ||
  ch == 'Y' ||
  ch == 'S' ||
  ch == 'W' ||
  ch == 'K' ||
  ch == 'M' ||
  ch == 'B' ||
  ch == 'D' ||
  ch == 'H' ||
  ch == 'V'
}

///|
fn is_origin_sequence_line(line : String) -> Bool {
  let trimmed = line.trim().to_owned()
  for ch in trimmed {
    return ch >= '0' && ch <= '9'
  }
  false
}

///|
fn parse_features(lines : Array[String]) -> Array[Feature] raise GenBankError {
  match lines.search_by(fn(line) { line.has_prefix("FEATURES") }) {
    Some(start) => {
      let features : Array[Feature] = []
      let mut current_key = ""
      let mut current_location = ""
      let mut qualifiers : Array[Qualifier] = []
      for i = start + 1; i < lines.length(); i = i + 1 {
        let line = lines[i]
        if line.has_prefix("ORIGIN") || line.has_prefix("//") {
          break
        }
        if is_feature_start(line) {
          if current_key != "" {
            features.push(
              make_feature(current_key, current_location, qualifiers),
            )
          }
          let parsed = parse_feature_head(line)
          current_key = parsed.0
          current_location = parsed.1
          qualifiers = []
        } else if is_qualifier_line(line) {
          qualifiers.push(parse_qualifier(line))
        } else if current_key != "" {
          let extra = line.trim().to_owned()
          if extra != "" &&
            !extra.has_prefix("/") &&
            append_qualifier_continuation(qualifiers, extra) {
            ()
          } else if extra != "" && !extra.has_prefix("/") {
            current_location = current_location + extra
          }
        }
      }
      if current_key != "" {
        features.push(make_feature(current_key, current_location, qualifiers))
      }
      features
    }
    None => raise GenBankError::MissingSection("FEATURES")
  }
}

///|
fn make_feature(
  key : String,
  location_text : String,
  qualifiers : Array[Qualifier],
) -> Feature raise GenBankError {
  let location = parse_location(location_text)
  { key, location, qualifiers }
}

///|
fn is_feature_start(line : String) -> Bool {
  line.length() > 21 &&
  line.has_prefix("     ") &&
  line[5] != ' ' &&
  !line.trim().to_owned().has_prefix("/")
}

///|
fn is_qualifier_line(line : String) -> Bool {
  line.trim().to_owned().has_prefix("/")
}

///|
fn parse_feature_head(line : String) -> (String, String) {
  let rest = line.unsafe_substring(start=5, end=line.length())
  let parts = split_words(rest)
  if parts.length() == 0 {
    ("", "")
  } else {
    let key = parts[0]
    let location = if parts.length() == 1 {
      ""
    } else {
      parts.view(start=1).join("")
    }
    (key, location)
  }
}

///|
fn parse_qualifier(line : String) -> Qualifier {
  let trimmed = line.trim().to_owned()
  match trimmed.split_once("=") {
    Some((left, right)) => {
      let left_text = left.to_owned()
      let name = left_text.unsafe_substring(start=1, end=left_text.length())
      let value = strip_quotes(right.to_owned())
      { name, value: Some(value) }
    }
    None => {
      let name = trimmed.unsafe_substring(start=1, end=trimmed.length())
      { name, value: None }
    }
  }
}

///|
fn append_qualifier_continuation(
  qualifiers : Array[Qualifier],
  extra : String,
) -> Bool {
  match qualifiers.last() {
    Some(last) =>
      match last.value {
        Some(value) => {
          let index = qualifiers.length() - 1
          let joined = value + strip_quotes(extra)
          qualifiers[index] = { name: last.name, value: Some(joined) }
          true
        }
        None => false
      }
    None => false
  }
}

///|
fn strip_quotes(text : String) -> String {
  let trimmed = text.trim().to_owned()
  let without_left = if trimmed.has_prefix("\"") {
    trimmed.unsafe_substring(start=1, end=trimmed.length())
  } else {
    trimmed
  }
  if without_left.has_suffix("\"") {
    without_left.unsafe_substring(start=0, end=without_left.length() - 1)
  } else {
    without_left
  }
}