///|
/// Porter2 English stemming filter compatible with Tantivy's `en_stem` era.
///
/// The filter expects lowercase English terms, so the built-in `en_stem`
/// pipeline places LowerCaseFilter before it. Non-ASCII and mixed-script terms
/// pass through unchanged.
pub struct EnglishStemmerFilter {}

///|
priv struct EnglishStemmerTokenStream {
  input : &TokenStream
  mut current : Token?
}

///|
pub fn EnglishStemmerFilter::new() -> EnglishStemmerFilter {
  EnglishStemmerFilter::{  }
}

///|
fn english_is_vowel(word : Array[Char], index : Int) -> Bool {
  if index < 0 || index >= word.length() {
    return false
  }
  match word[index] {
    'a' | 'e' | 'i' | 'o' | 'u' | 'y' => true
    _ => false
  }
}

///|
fn english_word_supported(text : String) -> Bool {
  if text.length() == 0 {
    return false
  }
  for ch in text {
    if !((ch >= 'a' && ch <= 'z') || ch.to_int() == 0x27) {
      return false
    }
  }
  true
}

///|
fn chars_start_with(word : Array[Char], prefix : String) -> Bool {
  let expected = prefix.to_array()
  if expected.length() > word.length() {
    return false
  }
  for index in 0.. Bool {
  let expected = suffix.to_array()
  if expected.length() > word.length() {
    return false
  }
  let offset = word.length() - expected.length()
  for index in 0.. Array[Char] {
  let suffix_length = suffix.length()
  let output : Array[Char] = []
  let prefix_length = word.length() - suffix_length
  for index in 0.. Array[Char] {
  let output : Array[Char] = []
  for index in 0.. Int {
  if start >= word.length() {
    return word.length()
  }
  for index in (start + 1).. Int {
  if chars_start_with(word, "gener") {
    5
  } else if chars_start_with(word, "commun") {
    6
  } else if chars_start_with(word, "arsen") {
    5
  } else {
    english_region(word, 0)
  }
}

///|
fn suffix_in_region(word : Array[Char], suffix : String, region : Int) -> Bool {
  chars_end_with(word, suffix) && word.length() - suffix.length() >= region
}

///|
fn english_contains_vowel(word : Array[Char], end : Int) -> Bool {
  for index in 0.. Bool {
  let length = word.length()
  if length == 2 {
    return english_is_vowel(word, 0) && !english_is_vowel(word, 1)
  }
  if length < 3 {
    return false
  }
  let last = word[length - 1]
  !english_is_vowel(word, length - 3) &&
  english_is_vowel(word, length - 2) &&
  !english_is_vowel(word, length - 1) &&
  last != 'w' &&
  last != 'x' &&
  last != 'Y'
}

///|
fn english_exception_one(word : String) -> String? {
  match word {
    "skis" => Some("ski")
    "skies" => Some("sky")
    "dying" => Some("die")
    "lying" => Some("lie")
    "tying" => Some("tie")
    "idly" => Some("idl")
    "gently" => Some("gentl")
    "ugly" => Some("ugli")
    "early" => Some("earli")
    "only" => Some("onli")
    "singly" => Some("singl")
    "sky" | "news" | "howe" | "atlas" | "cosmos" | "bias" | "andes" =>
      Some(word)
    _ => None
  }
}

///|
fn english_exception_two(word : Array[Char]) -> Bool {
  let text = String::from_array(word[:])
  text == "inning" ||
  text == "outing" ||
  text == "canning" ||
  text == "herring" ||
  text == "earring" ||
  text == "proceed" ||
  text == "exceed" ||
  text == "succeed"
}

///|
fn english_step_zero(word : Array[Char]) -> Array[Char] {
  if chars_end_with(word, "'s'") {
    replace_english_suffix(word, "'s'", "")
  } else if chars_end_with(word, "'s") {
    replace_english_suffix(word, "'s", "")
  } else if chars_end_with(word, "'") {
    replace_english_suffix(word, "'", "")
  } else {
    word
  }
}

///|
fn english_step_one_a(word : Array[Char]) -> Array[Char] {
  if chars_end_with(word, "sses") {
    return replace_english_suffix(word, "sses", "ss")
  }
  if chars_end_with(word, "ied") || chars_end_with(word, "ies") {
    let suffix = if chars_end_with(word, "ied") { "ied" } else { "ies" }
    let replacement = if word.length() - suffix.length() > 1 {
      "i"
    } else {
      "ie"
    }
    return replace_english_suffix(word, suffix, replacement)
  }
  if chars_end_with(word, "us") || chars_end_with(word, "ss") {
    return word
  }
  if chars_end_with(word, "s") &&
    word.length() > 2 &&
    english_contains_vowel(word, word.length() - 2) {
    return replace_english_suffix(word, "s", "")
  }
  word
}

///|
fn english_step_one_b(original : Array[Char], r1 : Int) -> Array[Char] {
  let mut word = original
  if chars_end_with(word, "eedly") {
    if suffix_in_region(word, "eedly", r1) {
      return replace_english_suffix(word, "eedly", "ee")
    }
    return word
  }
  if chars_end_with(word, "eed") {
    if suffix_in_region(word, "eed", r1) {
      return replace_english_suffix(word, "eed", "ee")
    }
    return word
  }
  let suffix = if chars_end_with(word, "ingly") {
    "ingly"
  } else if chars_end_with(word, "edly") {
    "edly"
  } else if chars_end_with(word, "ing") {
    "ing"
  } else if chars_end_with(word, "ed") {
    "ed"
  } else {
    ""
  }
  if suffix == "" {
    return word
  }
  let stem_length = word.length() - suffix.length()
  if !english_contains_vowel(word, stem_length) {
    return word
  }
  word = truncate_english_word(word, stem_length)
  if chars_end_with(word, "at") ||
    chars_end_with(word, "bl") ||
    chars_end_with(word, "iz") {
    return replace_english_suffix(word, "", "e")
  }
  if chars_end_with(word, "bb") ||
    chars_end_with(word, "dd") ||
    chars_end_with(word, "ff") ||
    chars_end_with(word, "gg") ||
    chars_end_with(word, "mm") ||
    chars_end_with(word, "nn") ||
    chars_end_with(word, "pp") ||
    chars_end_with(word, "rr") ||
    chars_end_with(word, "tt") {
    return truncate_english_word(word, word.length() - 1)
  }
  if r1 >= word.length() && english_short_syllable(word) {
    return replace_english_suffix(word, "", "e")
  }
  word
}

///|
fn english_step_one_c(word : Array[Char]) -> Array[Char] {
  if word.length() > 2 {
    let final_index = word.length() - 1
    if (word[final_index] == 'y' || word[final_index] == 'Y') &&
      !english_is_vowel(word, final_index - 1) {
      let output = truncate_english_word(word, final_index)
      output.push('i')
      return output
    }
  }
  word
}

///|
fn english_step_two(original : Array[Char], r1 : Int) -> Array[Char] {
  let word = original
  if suffix_in_region(word, "ization", r1) {
    replace_english_suffix(word, "ization", "ize")
  } else if suffix_in_region(word, "ational", r1) {
    replace_english_suffix(word, "ational", "ate")
  } else if suffix_in_region(word, "fulness", r1) {
    replace_english_suffix(word, "fulness", "ful")
  } else if suffix_in_region(word, "ousness", r1) {
    replace_english_suffix(word, "ousness", "ous")
  } else if suffix_in_region(word, "iveness", r1) {
    replace_english_suffix(word, "iveness", "ive")
  } else if suffix_in_region(word, "tional", r1) {
    replace_english_suffix(word, "tional", "tion")
  } else if suffix_in_region(word, "biliti", r1) {
    replace_english_suffix(word, "biliti", "ble")
  } else if suffix_in_region(word, "lessli", r1) {
    replace_english_suffix(word, "lessli", "less")
  } else if suffix_in_region(word, "entli", r1) {
    replace_english_suffix(word, "entli", "ent")
  } else if suffix_in_region(word, "ation", r1) {
    replace_english_suffix(word, "ation", "ate")
  } else if suffix_in_region(word, "alism", r1) {
    replace_english_suffix(word, "alism", "al")
  } else if suffix_in_region(word, "aliti", r1) {
    replace_english_suffix(word, "aliti", "al")
  } else if suffix_in_region(word, "fulli", r1) {
    replace_english_suffix(word, "fulli", "ful")
  } else if suffix_in_region(word, "ousli", r1) {
    replace_english_suffix(word, "ousli", "ous")
  } else if suffix_in_region(word, "iviti", r1) {
    replace_english_suffix(word, "iviti", "ive")
  } else if suffix_in_region(word, "enci", r1) {
    replace_english_suffix(word, "enci", "ence")
  } else if suffix_in_region(word, "anci", r1) {
    replace_english_suffix(word, "anci", "ance")
  } else if suffix_in_region(word, "abli", r1) {
    replace_english_suffix(word, "abli", "able")
  } else if suffix_in_region(word, "izer", r1) {
    replace_english_suffix(word, "izer", "ize")
  } else if suffix_in_region(word, "ator", r1) {
    replace_english_suffix(word, "ator", "ate")
  } else if suffix_in_region(word, "alli", r1) {
    replace_english_suffix(word, "alli", "al")
  } else if suffix_in_region(word, "bli", r1) {
    replace_english_suffix(word, "bli", "ble")
  } else if suffix_in_region(word, "ogi", r1) {
    let prefix_index = word.length() - "ogi".length() - 1
    if prefix_index >= 0 && word[prefix_index] == 'l' {
      replace_english_suffix(word, "ogi", "og")
    } else {
      word
    }
  } else if suffix_in_region(word, "li", r1) {
    let prefix_index = word.length() - 3
    if prefix_index >= 0 {
      match word[prefix_index] {
        'c' | 'd' | 'e' | 'g' | 'h' | 'k' | 'm' | 'n' | 'r' | 't' =>
          replace_english_suffix(word, "li", "")
        _ => word
      }
    } else {
      word
    }
  } else {
    word
  }
}

///|
fn english_step_three(word : Array[Char], r1 : Int, r2 : Int) -> Array[Char] {
  if suffix_in_region(word, "ational", r1) {
    replace_english_suffix(word, "ational", "ate")
  } else if suffix_in_region(word, "tional", r1) {
    replace_english_suffix(word, "tional", "tion")
  } else if suffix_in_region(word, "alize", r1) {
    replace_english_suffix(word, "alize", "al")
  } else if suffix_in_region(word, "icate", r1) {
    replace_english_suffix(word, "icate", "ic")
  } else if suffix_in_region(word, "iciti", r1) {
    replace_english_suffix(word, "iciti", "ic")
  } else if suffix_in_region(word, "ical", r1) {
    replace_english_suffix(word, "ical", "ic")
  } else if suffix_in_region(word, "ful", r1) {
    replace_english_suffix(word, "ful", "")
  } else if suffix_in_region(word, "ness", r1) {
    replace_english_suffix(word, "ness", "")
  } else if suffix_in_region(word, "ative", r2) {
    replace_english_suffix(word, "ative", "")
  } else {
    word
  }
}

///|
fn english_step_four(word : Array[Char], r2 : Int) -> Array[Char] {
  if suffix_in_region(word, "ement", r2) {
    replace_english_suffix(word, "ement", "")
  } else if suffix_in_region(word, "ance", r2) {
    replace_english_suffix(word, "ance", "")
  } else if suffix_in_region(word, "ence", r2) {
    replace_english_suffix(word, "ence", "")
  } else if suffix_in_region(word, "able", r2) {
    replace_english_suffix(word, "able", "")
  } else if suffix_in_region(word, "ible", r2) {
    replace_english_suffix(word, "ible", "")
  } else if suffix_in_region(word, "ment", r2) {
    replace_english_suffix(word, "ment", "")
  } else if suffix_in_region(word, "ant", r2) {
    replace_english_suffix(word, "ant", "")
  } else if suffix_in_region(word, "ent", r2) {
    replace_english_suffix(word, "ent", "")
  } else if suffix_in_region(word, "ism", r2) {
    replace_english_suffix(word, "ism", "")
  } else if suffix_in_region(word, "ate", r2) {
    replace_english_suffix(word, "ate", "")
  } else if suffix_in_region(word, "iti", r2) {
    replace_english_suffix(word, "iti", "")
  } else if suffix_in_region(word, "ous", r2) {
    replace_english_suffix(word, "ous", "")
  } else if suffix_in_region(word, "ive", r2) {
    replace_english_suffix(word, "ive", "")
  } else if suffix_in_region(word, "ize", r2) {
    replace_english_suffix(word, "ize", "")
  } else if suffix_in_region(word, "al", r2) {
    replace_english_suffix(word, "al", "")
  } else if suffix_in_region(word, "er", r2) {
    replace_english_suffix(word, "er", "")
  } else if suffix_in_region(word, "ic", r2) {
    replace_english_suffix(word, "ic", "")
  } else if suffix_in_region(word, "ion", r2) {
    let prefix_index = word.length() - 4
    if prefix_index >= 0 &&
      (word[prefix_index] == 's' || word[prefix_index] == 't') {
      replace_english_suffix(word, "ion", "")
    } else {
      word
    }
  } else {
    word
  }
}

///|
fn english_step_five(word : Array[Char], r1 : Int, r2 : Int) -> Array[Char] {
  if chars_end_with(word, "e") {
    let suffix_index = word.length() - 1
    if suffix_index >= r2 {
      return truncate_english_word(word, suffix_index)
    }
    if suffix_index >= r1 {
      let stem = truncate_english_word(word, suffix_index)
      if !english_short_syllable(stem) {
        return stem
      }
    }
    return word
  }
  if chars_end_with(word, "ll") && word.length() - 1 >= r2 {
    return truncate_english_word(word, word.length() - 1)
  }
  word
}

///|
fn restore_english_y(word : Array[Char]) -> Array[Char] {
  let output : Array[Char] = []
  for ch in word {
    output.push(if ch == 'Y' { 'y' } else { ch })
  }
  output
}

///|
fn stem_english_word(text : String) -> String {
  if !english_word_supported(text) {
    return text
  }
  let mut word = text.to_array()
  if word.length() > 0 && word[0].to_int() == 0x27 {
    let without_apostrophe : Array[Char] = []
    for index in 1.. return stem
    None => ()
  }
  if word[0] == 'y' {
    word[0] = 'Y'
  }
  for index in 1.. {
      self.current = Some({
        text: stem_english_word(token.text),
        position: token.position,
        position_length: token.position_length,
        start_offset: token.start_offset,
        end_offset: token.end_offset,
      })
      true
    }
    None => {
      self.current = None
      false
    }
  }
}

///|
impl TokenStream for EnglishStemmerTokenStream with fn token(self) {
  self.current
}

///|
pub impl TokenFilter for EnglishStemmerFilter with fn transform(_self, input) {
  EnglishStemmerTokenStream::{ input, current: None }
}

///|
/// Creates the same high-level pipeline as Tantivy's `en_stem` preset.
pub fn english_stem_analyzer() -> TextAnalyzer {
  let analyzer = TextAnalyzer::new(SimpleTokenizer::new())
  analyzer.add_filter(RemoveLongFilter::new(40))
  analyzer.add_filter(LowerCaseFilter::new())
  analyzer.add_filter(EnglishStemmerFilter::new())
  analyzer
}