///|
/// Parse a GenBank location expression into ordered, strand-aware segments.
pub fn parse_location(text : String) -> Location raise GenBankError {
  let raw = text.trim().to_owned()
  if raw == "" {
    raise GenBankError::InvalidLocation(raw)
  }
  let segments = parse_location_expr(raw)
  if segments.is_empty() {
    raise GenBankError::InvalidLocation(raw)
  }
  let ranges = segments.map(fn(segment) { segment.range })
  {
    raw,
    ranges,
    complement: raw.has_prefix("complement(") && raw.has_suffix(")"),
    joined: raw.has_prefix("join(") && raw.has_suffix(")"),
    ordered: raw.has_prefix("order(") && raw.has_suffix(")"),
    segments,
  }
}

///|
fn parse_location_expr(
  text : String,
) -> Array[LocationSegment] raise GenBankError {
  let s = text.trim().to_owned()
  if has_wrapper(s, "complement(") {
    let inner = unwrap_wrapper(s, "complement(")
    let original = parse_location_expr(inner)
    let segments : Array[LocationSegment] = []
    for i = original.length() - 1; i >= 0; i = i - 1 {
      let segment = original[i]
      segments.push({
        range: segment.range,
        reverse: !segment.reverse,
        remote: segment.remote,
      })
    }
    return segments
  }
  if has_wrapper(s, "join(") || has_wrapper(s, "order(") {
    let prefix = if s.has_prefix("join(") { "join(" } else { "order(" }
    let inner = unwrap_wrapper(s, prefix)
    let segments : Array[LocationSegment] = []
    for part in split_top_level(inner, 44) {
      if part.trim().to_owned() == "" {
        raise GenBankError::InvalidLocation(s)
      }
      for segment in parse_location_expr(part) {
        segments.push(segment)
      }
    }
    return segments
  }
  if s.has_prefix("(") || s.has_suffix(")") {
    raise GenBankError::InvalidLocation(s)
  }
  let remote_and_body = split_remote_accession(s)
  let range = parse_range(remote_and_body.1)
  [{ range, reverse: false, remote: remote_and_body.0 }]
}

///|
fn has_wrapper(text : String, prefix : String) -> Bool {
  text.has_prefix(prefix) &&
  text.has_suffix(")") &&
  matching_close(text, prefix.length() - 1)
}

///|
fn unwrap_wrapper(text : String, prefix : String) -> String {
  text.unsafe_substring(start=prefix.length(), end=text.length() - 1)
}

///|
fn matching_close(text : String, open_index : Int) -> Bool {
  let mut depth = 0
  for i = open_index; i < text.length(); i = i + 1 {
    match text[i] {
      '(' => depth = depth + 1
      ')' => {
        depth = depth - 1
        if depth == 0 && i != text.length() - 1 {
          return false
        }
      }
      _ => ()
    }
  }
  depth == 0
}

///|
fn split_top_level(text : String, separator : Int) -> Array[String] {
  let parts : Array[String] = []
  let mut depth = 0
  let mut start = 0
  for i = 0; i < text.length(); i = i + 1 {
    match text[i] {
      '(' => depth = depth + 1
      ')' => depth = depth - 1
      ch if ch.to_int() == separator && depth == 0 => {
        parts.push(text.unsafe_substring(start~, end=i))
        start = i + 1
      }
      _ => ()
    }
  }
  parts.push(text.unsafe_substring(start~, end=text.length()))
  parts
}

///|
fn split_remote_accession(text : String) -> (String?, String) {
  let parts = split_top_level(text, 58)
  if parts.length() == 2 && !parts[0].contains("(") {
    (Some(parts[0].trim().to_owned()), parts[1].trim().to_owned())
  } else {
    (None, text.trim().to_owned())
  }
}

///|
fn parse_range(text : String) -> Range raise GenBankError {
  let s = text.trim().to_owned()
  let has_partial_start = s.has_prefix("<") || s.has_prefix(">")
  let has_partial_end = s.has_suffix(">") || s.has_suffix("<")
  let is_between = s.contains("^")
  match s.split_once("..") {
    Some((left, right)) => {
      let start = parse_endpoint(left.to_owned())
      let end = parse_endpoint(right.to_owned())
      make_range(
        start,
        end,
        has_partial_start || left.contains("<"),
        has_partial_end || right.contains(">"),
        false,
        s,
      )
    }
    None =>
      match s.split_once("^") {
        Some((left, right)) => {
          let start = parse_endpoint(left.to_owned())
          let end = parse_endpoint(right.to_owned())
          make_range(
            start,
            end,
            has_partial_start || left.contains("<"),
            has_partial_end || right.contains(">"),
            true,
            s,
          )
        }
        None => {
          let point = parse_endpoint(s)
          make_range(
            point, point, has_partial_start, has_partial_end, is_between, text,
          )
        }
      }
  }
}

///|
fn make_range(
  start : Int,
  end : Int,
  partial_start : Bool,
  partial_end : Bool,
  between : Bool,
  original : String,
) -> Range raise GenBankError {
  if start < 1 || end < 1 || start > end {
    raise GenBankError::InvalidLocation(original)
  }
  { start, end, partial_start, partial_end, between }
}

///|
fn parse_endpoint(text : String) -> Int raise GenBankError {
  let s = text.trim().to_owned()
  let without_marker = if s.has_prefix("<") || s.has_prefix(">") {
    s.unsafe_substring(start=1, end=s.length())
  } else {
    s
  }
  if without_marker.has_prefix("one-of(") && without_marker.has_suffix(")") {
    let inner = unwrap_wrapper(without_marker, "one-of(")
    match split_top_level(inner, 44).get(0) {
      Some(first) => parse_endpoint(first)
      None => raise GenBankError::InvalidLocation(text)
    }
  } else {
    parse_int(without_marker)
  }
}