///| Trailer parsing and manipulation for commit messages

///|
/// Check if a line is a trailer line (format "Key: Value" or "Key = Value").
fn trailers_is_trailer(line : String) -> Bool {
  match line.find(": ") {
    Some(idx) => {
      if idx == 0 {
        return false
      }
      let key = String::unsafe_substring(line, start=0, end=idx)
      for c in key.to_array() {
        if c == ' ' || c == '\t' {
          return false
        }
      }
      true
    }
    None =>
      match line.find(" = ") {
        Some(idx) => {
          if idx == 0 {
            return false
          }
          let key = String::unsafe_substring(line, start=0, end=idx)
          for c in key.to_array() {
            if c == ' ' || c == '\t' {
              return false
            }
          }
          true
        }
        None => false
      }
  }
}

///|
fn trailers_trim(s : String) -> String {
  let mut start = 0
  let mut end = s.length()
  while start < end {
    let c = s[start]
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      start += 1
    } else {
      break
    }
  }
  while end > start {
    let c = s[end - 1]
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      end -= 1
    } else {
      break
    }
  }
  if start == 0 && end == s.length() {
    s
  } else {
    String::unsafe_substring(s, start~, end~)
  }
}

///|
/// Normalize a trailer string to "Key: Value" form.
fn trailers_normalize(trailer : String) -> String {
  match trailer.find(": ") {
    Some(_) => trailer
    None =>
      match trailer.find(":") {
        Some(idx) => {
          let key = String::unsafe_substring(trailer, start=0, end=idx)
          let value = trailers_trim(
            String::unsafe_substring(
              trailer,
              start=idx + 1,
              end=trailer.length(),
            ),
          )
          "\{key}: \{value}"
        }
        None =>
          match trailer.find("=") {
            Some(idx) => {
              let key = trailers_trim(
                String::unsafe_substring(trailer, start=0, end=idx),
              )
              let value = trailers_trim(
                String::unsafe_substring(
                  trailer,
                  start=idx + 1,
                  end=trailer.length(),
                ),
              )
              "\{key}: \{value}"
            }
            None => trailer
          }
      }
  }
}

///|
/// Parse trailer lines from the end of a commit message.
/// Returns an array of (key, value) pairs.
pub fn parse_trailers(message : String) -> Array[(String, String)] {
  let lines : Array[String] = []
  for line_view in message.split("\n") {
    lines.push(line_view.to_owned())
  }
  // Remove trailing empty lines
  while lines.length() > 0 &&
        trailers_trim(lines[lines.length() - 1]).length() == 0 {
    ignore(lines.pop())
  }
  // Walk backwards to find trailer block
  let raw_trailers : Array[String] = []
  let mut i = lines.length() - 1
  while i >= 0 {
    let line = lines[i]
    if trailers_is_trailer(line) {
      raw_trailers.push(line)
      i -= 1
    } else {
      break
    }
  }
  let raw_trailers = raw_trailers.rev()
  // Parse each trailer into (key, value)
  let result : Array[(String, String)] = []
  for trailer in raw_trailers {
    match trailer.find(": ") {
      Some(idx) => {
        let key = String::unsafe_substring(trailer, start=0, end=idx)
        let value = String::unsafe_substring(
          trailer,
          start=idx + 2,
          end=trailer.length(),
        )
        result.push((key, value))
      }
      None =>
        match trailer.find(" = ") {
          Some(idx) => {
            let key = String::unsafe_substring(trailer, start=0, end=idx)
            let value = String::unsafe_substring(
              trailer,
              start=idx + 3,
              end=trailer.length(),
            )
            result.push((key, value))
          }
          None => ()
        }
    }
  }
  result
}

///|
/// Split a message into (body, trailer_lines).
fn trailers_split(input : String) -> (String, Array[String]) {
  let lines : Array[String] = []
  for line_view in input.split("\n") {
    lines.push(line_view.to_owned())
  }
  // Remove trailing empty line from split
  if lines.length() > 0 && lines[lines.length() - 1].length() == 0 {
    ignore(lines.pop())
  }
  // Walk backwards to find trailer block
  let trailer_start = {
    let mut idx = lines.length()
    let mut i = lines.length() - 1
    while i >= 0 {
      let line = lines[i]
      if trailers_is_trailer(line) {
        idx = i
        i -= 1
      } else if trailers_trim(line).length() == 0 {
        break
      } else {
        break
      }
    }
    idx
  }
  let trailers : Array[String] = []
  let mut ti = trailer_start
  while ti < lines.length() {
    trailers.push(lines[ti])
    ti += 1
  }
  let body_sb = StringBuilder::new()
  let mut bi = 0
  while bi < trailer_start {
    body_sb.write_string(lines[bi])
    body_sb.write_char('\n')
    bi += 1
  }
  (body_sb.to_string(), trailers)
}

///|
/// Add a trailer to a commit message. If the message already has trailers,
/// the new trailer is appended after the existing ones.
pub fn add_trailer(message : String, key : String, value : String) -> String {
  let (body, existing) = trailers_split(message)
  let all_trailers : Array[String] = []
  all_trailers.append(existing)
  all_trailers.push(trailers_normalize("\{key}: \{value}"))
  let result = StringBuilder::new()
  result.write_string(body)
  // Ensure blank line before trailers if body doesn't end with one
  if body.length() > 0 && !body.has_suffix("\n\n") {
    if body.has_suffix("\n") {
      result.write_char('\n')
    } else {
      result.write_string("\n\n")
    }
  }
  for trailer in all_trailers {
    result.write_string(trailer)
    result.write_char('\n')
  }
  result.to_string()
}