///|
fn metaphone_is_blocking_h_predecessor(code : UInt16) -> Bool {
  code == 67 || code == 71 || code == 80 || code == 83 || code == 84
}

///|
fn metaphone_is_soft_g_follower(code : UInt16) -> Bool {
  code == 69 || code == 73 || code == 89
}

///|
fn metaphone_prepare_cursor(letters : String) -> RuleCursor {
  let maximum = letters.length() * 2 + 4
  let cursor = RuleCursor::new(letters, maximum)
  if ascii_starts_with_at(letters, 0, "AE") {
    cursor.advance(1)
  } else if ascii_starts_with_at(letters, 0, "GN") ||
    ascii_starts_with_at(letters, 0, "KN") ||
    ascii_starts_with_at(letters, 0, "PN") ||
    ascii_starts_with_at(letters, 0, "WR") {
    cursor.advance(1)
  } else if ascii_starts_with_at(letters, 0, "WH") {
    cursor.append("W").advance(2)
  } else if ascii_starts_with_at(letters, 0, "X") {
    cursor.append("S").advance(1)
  } else {
    cursor
  }
}

///|
fn metaphone_encode_c(cursor : RuleCursor) -> RuleCursor {
  let previous = cursor.peek(-1)
  let next = cursor.peek(1)
  if ascii_starts_with_at(cursor.input, cursor.index, "CIA") {
    cursor.append("X").advance(3)
  } else if ascii_starts_with_at(cursor.input, cursor.index, "CH") {
    if previous == 83 {
      cursor.append("K").advance(2)
    } else {
      cursor.append("X").advance(2)
    }
  } else if metaphone_is_soft_g_follower(next) {
    if previous == 83 {
      cursor.advance(1)
    } else {
      cursor.append("S").advance(1)
    }
  } else {
    cursor.append("K").advance(1)
  }
}

///|
fn metaphone_encode_g(cursor : RuleCursor) -> RuleCursor {
  let previous = cursor.peek(-1)
  let next = cursor.peek(1)
  if next == 72 {
    if previous == 85 {
      cursor.append("F").advance(2)
    } else {
      cursor.advance(2)
    }
  } else if next == 78 &&
    (
      cursor.index + 2 == cursor.input.length() ||
      ascii_ends_with(cursor.input, "GNED")
    ) {
    cursor.advance(1)
  } else if metaphone_is_soft_g_follower(next) {
    cursor.append("J").advance(1)
  } else {
    cursor.append("K").advance(1)
  }
}

///|
fn metaphone_encode_s(cursor : RuleCursor) -> RuleCursor {
  if ascii_starts_with_at(cursor.input, cursor.index, "SH") {
    cursor.append("X").advance(2)
  } else if ascii_starts_with_at(cursor.input, cursor.index, "SIO") ||
    ascii_starts_with_at(cursor.input, cursor.index, "SIA") {
    cursor.append("X").advance(3)
  } else {
    cursor.append("S").advance(1)
  }
}

///|
fn metaphone_encode_t(cursor : RuleCursor) -> RuleCursor {
  if ascii_starts_with_at(cursor.input, cursor.index, "TIA") ||
    ascii_starts_with_at(cursor.input, cursor.index, "TIO") {
    cursor.append("X").advance(3)
  } else if ascii_starts_with_at(cursor.input, cursor.index, "TH") {
    cursor.append("0").advance(2)
  } else if ascii_starts_with_at(cursor.input, cursor.index, "TCH") {
    cursor.advance(1)
  } else {
    cursor.append("T").advance(1)
  }
}

///|
fn metaphone_step(cursor : RuleCursor) -> RuleCursor {
  let current = cursor.peek(0)
  let previous = cursor.peek(-1)
  let next = cursor.peek(1)
  if cursor.index > 0 && current == previous && current != 67 {
    return cursor.advance(1)
  }
  match current {
    65 | 69 | 73 | 79 | 85 =>
      if cursor.output.length() == 0 {
        cursor.append(current.unsafe_to_char().to_string()).advance(1)
      } else {
        cursor.advance(1)
      }
    66 =>
      if previous == 77 && cursor.remaining() == 1 {
        cursor.advance(1)
      } else {
        cursor.append("B").advance(1)
      }
    67 => metaphone_encode_c(cursor)
    68 =>
      if next == 71 && metaphone_is_soft_g_follower(cursor.peek(2)) {
        cursor.append("J").advance(3)
      } else {
        cursor.append("T").advance(1)
      }
    70 => cursor.append("F").advance(1)
    71 => metaphone_encode_g(cursor)
    72 =>
      if encoder_ascii_is_vowel(next) &&
        !metaphone_is_blocking_h_predecessor(previous) {
        cursor.append("H").advance(1)
      } else {
        cursor.advance(1)
      }
    74 => cursor.append("J").advance(1)
    75 =>
      if previous == 67 {
        cursor.advance(1)
      } else {
        cursor.append("K").advance(1)
      }
    76 => cursor.append("L").advance(1)
    77 => cursor.append("M").advance(1)
    78 => cursor.append("N").advance(1)
    80 =>
      if next == 72 {
        cursor.append("F").advance(2)
      } else {
        cursor.append("P").advance(1)
      }
    81 => cursor.append("K").advance(1)
    82 => cursor.append("R").advance(1)
    83 => metaphone_encode_s(cursor)
    84 => metaphone_encode_t(cursor)
    86 => cursor.append("F").advance(1)
    87 | 89 =>
      if encoder_ascii_is_vowel(next) {
        cursor.append(current.unsafe_to_char().to_string()).advance(1)
      } else {
        cursor.advance(1)
      }
    88 => cursor.append("KS").advance(1)
    90 => cursor.append("S").advance(1)
    _ => cursor.advance(1)
  }
}

///|
/// Encodes ASCII letters with the original Metaphone rule families.
pub fn metaphone(input : String) -> String {
  let letters = normalize_ascii_letters(input)
  if letters.length() == 0 {
    return ""
  }
  let mut cursor = metaphone_prepare_cursor(letters)
  while cursor.remaining() > 0 {
    let next = metaphone_step(cursor)
    if next.index <= cursor.index {
      abort("Metaphone rule did not advance")
    }
    cursor = next
  }
  cursor.output
}