///|
pub(all) struct SpellResult {
  accepted : Bool
  stem : String?
  prefix : String?
  suffix : String?
}

///|
fn flag_tokens(flag_mode : String, flags : String) -> Array[String] {
  let result : Array[String] = []
  if flag_mode == "num" {
    for token in flags.split(",") {
      let value = token.to_owned()
      if !value.is_empty() {
        result.push(value)
      }
    }
  } else if flag_mode == "long" {
    let chars = flags.to_array()
    let mut i = 0
    while i + 1 < chars.length() {
      result.push(chars[i].to_string() + chars[i + 1].to_string())
      i += 2
    }
  } else {
    for c in flags {
      result.push(c.to_string())
    }
  }
  result
}

///|
fn expand_flag_vector(config : AffixConfig, flags : String) -> String {
  match parse_decimal_count(flags) {
    Some(index) =>
      if index > 0 && index <= config.flag_aliases.length() {
        config.flag_aliases[index - 1]
      } else {
        flags
      }
    None => flags
  }
}

///|
fn flags_contain(config : AffixConfig, flags : String, flag : String) -> Bool {
  if flag.is_empty() {
    false
  } else {
    flag_tokens(config.flag_mode, expand_flag_vector(config, flags)).contains(
      flag,
    )
  }
}

///|
fn entry_is_forbidden(config : AffixConfig, entry : DicEntry) -> Bool {
  match config.forbidden_word {
    Some(flag) => flags_contain(config, entry.flags, flag)
    None => false
  }
}

///|
fn entry_needs_affix(config : AffixConfig, entry : DicEntry) -> Bool {
  match config.need_affix {
    Some(flag) => flags_contain(config, entry.flags, flag)
    None => false
  }
}

///|
fn entry_keeps_case(config : AffixConfig, entry : DicEntry) -> Bool {
  match config.keep_case {
    Some(flag) => flags_contain(config, entry.flags, flag)
    None => false
  }
}

///|
fn entry_is_only_in_compound(config : AffixConfig, entry : DicEntry) -> Bool {
  match config.only_in_compound {
    Some(flag) => flags_contain(config, entry.flags, flag)
    None => false
  }
}

///|
fn entry_can_be_exact(config : AffixConfig, entry : DicEntry) -> Bool {
  !entry_is_forbidden(config, entry) &&
  !entry_needs_affix(config, entry) &&
  !entry_is_only_in_compound(config, entry)
}

///|
fn entry_can_be_affixed(config : AffixConfig, entry : DicEntry) -> Bool {
  !entry_is_forbidden(config, entry) &&
  !entry_is_only_in_compound(config, entry)
}

///|
fn rule_has_circumfix(config : AffixConfig, rule : AffixRule) -> Bool {
  match config.circumfix {
    Some(flag) => flags_contain(config, rule.continuation, flag)
    None => false
  }
}

///|
fn rule_needs_affix(config : AffixConfig, rule : AffixRule) -> Bool {
  match config.need_affix {
    Some(flag) => flags_contain(config, rule.continuation, flag)
    None => false
  }
}

///|
fn rule_is_only_in_compound(config : AffixConfig, rule : AffixRule) -> Bool {
  match config.only_in_compound {
    Some(flag) => flags_contain(config, rule.continuation, flag)
    None => false
  }
}

///|
fn entry_can_be_compound(config : AffixConfig, entry : DicEntry) -> Bool {
  !entry_is_forbidden(config, entry)
}

///|
fn condition_width(pattern : String) -> Int {
  let chars = pattern.to_array()
  let mut width = 0
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '[' {
      let mut j = i + 1
      while j < chars.length() && chars[j] != ']' {
        j += 1
      }
      if j >= chars.length() {
        return -1
      }
      width += 1
      i = j + 1
    } else {
      width += 1
      i += 1
    }
  }
  width
}

///|
fn condition_matches_at(
  pattern : String,
  text : Array[Char],
  start : Int,
) -> Bool {
  let pattern_chars = pattern.to_array()
  let mut pattern_index = 0
  let mut text_index = start
  while pattern_index < pattern_chars.length() {
    if pattern_chars[pattern_index] == '[' {
      let mut end = pattern_index + 1
      while end < pattern_chars.length() && pattern_chars[end] != ']' {
        end += 1
      }
      if end >= pattern_chars.length() || text_index >= text.length() {
        return false
      }
      let negate = pattern_index + 1 < end &&
        pattern_chars[pattern_index + 1] == '^'
      let char_start = if negate {
        pattern_index + 2
      } else {
        pattern_index + 1
      }
      let mut found = false
      for i in char_start..= text.length() {
        return false
      }
      pattern_index += 1
      text_index += 1
    } else {
      if text_index >= text.length() ||
        pattern_chars[pattern_index] != text[text_index] {
        return false
      }
      pattern_index += 1
      text_index += 1
    }
  }
  true
}

///|
fn condition_matches(rule : AffixRule, stem : String) -> Bool {
  let condition = rule.condition
  if condition.is_empty() || condition == "." {
    return true
  }
  let width = condition_width(condition)
  let chars = stem.to_array()
  if width < 0 || width > chars.length() {
    return false
  }
  let start = match rule.kind {
    Prefix => 0
    Suffix => chars.length() - width
  }
  condition_matches_at(condition, chars, start)
}

///|
fn ends_with(text : StringView, suffix : String) -> Bool {
  let text_len = text.length()
  let suffix_len = suffix.length()
  if suffix_len > text_len {
    return false
  }
  text.view(start_offset=text_len - suffix_len).to_owned() == suffix
}

///|
pub fn Dictionary::find_entries(
  self : Dictionary,
  word : String,
) -> Array[DicEntry] {
  match self.index.get(word) {
    Some(entries) => entries.copy()
    None => []
  }
}

///|
fn exact_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> SpellResult? {
  for entry in dictionary.find_entries(word) {
    if entry_can_be_exact(config, entry) {
      return Some({
        accepted: true,
        stem: Some(word),
        prefix: None,
        suffix: None,
      })
    }
  }
  None
}

///|
fn prefix_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  rule : AffixRule,
) -> SpellResult? {
  let add = apply_ignore(config, rule.add)
  let strip = apply_ignore(config, rule.strip)
  if !add.is_empty() && word.find(add) != Some(0) {
    return None
  }
  let remainder = word.view(start_offset=add.length())
  let stem = strip + remainder.to_owned()
  if !condition_matches(rule, stem) {
    return None
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, rule.flag) &&
      entry_can_be_affixed(config, entry) &&
      !rule_is_only_in_compound(config, rule) &&
      !rule_needs_affix(config, rule) &&
      !rule_has_circumfix(config, rule) {
      return Some({
        accepted: true,
        stem: Some(stem),
        prefix: Some(add),
        suffix: None,
      })
    }
  }
  None
}

///|
fn suffix_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  rule : AffixRule,
) -> SpellResult? {
  if !rule.add.is_empty() && !ends_with(word, rule.add) {
    return None
  }
  let stem = word.view(end_offset=word.length() - rule.add.length()).to_owned() +
    rule.strip
  if !condition_matches(rule, stem) {
    return None
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, rule.flag) &&
      entry_can_be_affixed(config, entry) &&
      !rule_is_only_in_compound(config, rule) &&
      !rule_needs_affix(config, rule) &&
      !rule_has_circumfix(config, rule) {
      return Some({
        accepted: true,
        stem: Some(stem),
        prefix: None,
        suffix: Some(rule.add),
      })
    }
  }
  None
}

///|
fn twofold_suffix_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  outer : AffixRule,
  inner : AffixRule,
) -> SpellResult? {
  if outer.add.is_empty() || inner.add.is_empty() {
    return None
  }
  if !ends_with(word, outer.add) {
    return None
  }
  let intermediate = word
    .view(end_offset=word.length() - outer.add.length())
    .to_owned()
  if !condition_matches(outer, intermediate) {
    return None
  }
  if !flags_contain(config, inner.continuation, outer.flag) {
    return None
  }
  if !ends_with(intermediate.view(), inner.add) {
    return None
  }
  let stem = intermediate
    .view(end_offset=intermediate.length() - inner.add.length())
    .to_owned() +
    inner.strip
  if !condition_matches(inner, stem) {
    return None
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, inner.flag) &&
      entry_can_be_affixed(config, entry) &&
      !rule_is_only_in_compound(config, inner) &&
      !rule_is_only_in_compound(config, outer) &&
      !rule_needs_affix(config, outer) {
      return Some({
        accepted: true,
        stem: Some(stem),
        prefix: None,
        suffix: Some(outer.add),
      })
    }
  }
  None
}

///|
fn twofold_prefix_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  outer : AffixRule,
  inner : AffixRule,
) -> SpellResult? {
  if outer.add.is_empty() ||
    inner.add.is_empty() ||
    word.find(outer.add) != Some(0) {
    return None
  }
  let intermediate = word.view(start_offset=outer.add.length()).to_owned()
  if !condition_matches(outer, intermediate) ||
    !flags_contain(config, inner.continuation, outer.flag) ||
    intermediate.find(inner.add) != Some(0) {
    return None
  }
  let stem = inner.strip +
    intermediate.view(start_offset=inner.add.length()).to_owned()
  if !condition_matches(inner, stem) {
    return None
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, inner.flag) &&
      entry_can_be_affixed(config, entry) {
      return Some({
        accepted: true,
        stem: Some(stem),
        prefix: Some(outer.add),
        suffix: None,
      })
    }
  }
  None
}

///|
fn cross_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  prefix : AffixRule,
  suffix : AffixRule,
) -> SpellResult? {
  if prefix.add.is_empty() || suffix.add.is_empty() {
    return None
  }
  if !prefix.cross_product || !suffix.cross_product {
    return None
  }
  if word.find(prefix.add) != Some(0) || !ends_with(word, suffix.add) {
    return None
  }
  if prefix.add.length() + suffix.add.length() >= word.length() {
    return None
  }
  let inner = word.view(
    start_offset=prefix.add.length(),
    end_offset=word.length() - suffix.add.length(),
  )
  let stem = prefix.strip + inner.to_owned() + suffix.strip
  if !condition_matches(prefix, stem) || !condition_matches(suffix, stem) {
    return None
  }
  for entry in dictionary.find_entries(stem) {
    let prefix_flag_ok = flags_contain(config, entry.flags, prefix.flag) ||
      flags_contain(config, suffix.continuation, prefix.flag)
    let suffix_flag_ok = flags_contain(config, entry.flags, suffix.flag) ||
      flags_contain(config, prefix.continuation, suffix.flag)
    if prefix_flag_ok &&
      suffix_flag_ok &&
      entry_can_be_affixed(config, entry) &&
      !(rule_needs_affix(config, prefix) && rule_needs_affix(config, suffix)) &&
      (
        !rule_has_circumfix(config, prefix) ||
        rule_has_circumfix(config, suffix)
      ) &&
      (
        !rule_has_circumfix(config, suffix) ||
        rule_has_circumfix(config, prefix)
      ) {
      return Some({
        accepted: true,
        stem: Some(stem),
        prefix: Some(prefix.add),
        suffix: Some(suffix.add),
      })
    }
  }
  None
}

///|
priv struct CompoundPartMatch {
  flags : String
  prefix : Bool
  suffix : Bool
  prefix_permit : Bool
  suffix_permit : Bool
  forbid : Bool
}

///|
fn affix_compound_permit(config : AffixConfig, rule : AffixRule) -> Bool {
  match config.compound_permit {
    Some(flag) => flags_contain(config, rule.continuation, flag)
    None => false
  }
}

///|
fn affix_compound_forbid(config : AffixConfig, rule : AffixRule) -> Bool {
  match config.compound_forbid {
    Some(flag) => flags_contain(config, rule.continuation, flag)
    None => false
  }
}

///|
fn entry_has_compound_forbid(config : AffixConfig, entry : DicEntry) -> Bool {
  match config.compound_forbid {
    Some(flag) => flags_contain(config, entry.flags, flag)
    None => false
  }
}

///|
fn exact_part_matches(
  dictionary : Dictionary,
  config : AffixConfig,
  part : String,
) -> Array[CompoundPartMatch] {
  let result : Array[CompoundPartMatch] = []
  for entry in dictionary.find_entries(part) {
    if entry_can_be_compound(config, entry) && !entry_needs_affix(config, entry) {
      result.push({
        flags: entry.flags,
        prefix: false,
        suffix: false,
        prefix_permit: false,
        suffix_permit: false,
        forbid: entry_has_compound_forbid(config, entry),
      })
    }
  }
  result
}

///|
fn prefix_part_matches(
  dictionary : Dictionary,
  config : AffixConfig,
  part : StringView,
  rule : AffixRule,
) -> Array[CompoundPartMatch] {
  let result : Array[CompoundPartMatch] = []
  if rule.add.is_empty() || part.find(rule.add) != Some(0) {
    return result
  }
  let stem = rule.strip + part.view(start_offset=rule.add.length()).to_owned()
  if !condition_matches(rule, stem) {
    return result
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, rule.flag) &&
      entry_can_be_compound(config, entry) {
      result.push({
        flags: entry.flags + rule.continuation,
        prefix: true,
        suffix: false,
        prefix_permit: affix_compound_permit(config, rule),
        suffix_permit: false,
        forbid: affix_compound_forbid(config, rule),
      })
    }
  }
  result
}

///|
fn suffix_part_matches(
  dictionary : Dictionary,
  config : AffixConfig,
  part : StringView,
  rule : AffixRule,
) -> Array[CompoundPartMatch] {
  let result : Array[CompoundPartMatch] = []
  if rule.add.is_empty() || !ends_with(part, rule.add) {
    return result
  }
  let stem = part.view(end_offset=part.length() - rule.add.length()).to_owned() +
    rule.strip
  if !condition_matches(rule, stem) {
    return result
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, rule.flag) &&
      entry_can_be_compound(config, entry) {
      result.push({
        flags: entry.flags + rule.continuation,
        prefix: false,
        suffix: true,
        prefix_permit: false,
        suffix_permit: affix_compound_permit(config, rule),
        forbid: affix_compound_forbid(config, rule),
      })
    }
  }
  result
}

///|
fn cross_part_matches(
  dictionary : Dictionary,
  config : AffixConfig,
  part : StringView,
  prefix : AffixRule,
  suffix : AffixRule,
) -> Array[CompoundPartMatch] {
  let result : Array[CompoundPartMatch] = []
  if prefix.add.is_empty() ||
    suffix.add.is_empty() ||
    !prefix.cross_product ||
    !suffix.cross_product ||
    part.find(prefix.add) != Some(0) ||
    !ends_with(part, suffix.add) ||
    prefix.add.length() + suffix.add.length() >= part.length() {
    return result
  }
  let inner = part.view(
    start_offset=prefix.add.length(),
    end_offset=part.length() - suffix.add.length(),
  )
  let stem = prefix.strip + inner.to_owned() + suffix.strip
  if !condition_matches(prefix, stem) || !condition_matches(suffix, stem) {
    return result
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, prefix.flag) &&
      flags_contain(config, entry.flags, suffix.flag) &&
      entry_can_be_compound(config, entry) {
      result.push({
        flags: entry.flags + prefix.continuation + suffix.continuation,
        prefix: true,
        suffix: true,
        prefix_permit: affix_compound_permit(config, prefix),
        suffix_permit: affix_compound_permit(config, suffix),
        forbid: affix_compound_forbid(config, prefix) ||
        affix_compound_forbid(config, suffix),
      })
    }
  }
  result
}

///|
fn twofold_suffix_part_matches(
  dictionary : Dictionary,
  config : AffixConfig,
  part : StringView,
  outer : AffixRule,
  inner : AffixRule,
) -> Array[CompoundPartMatch] {
  let result : Array[CompoundPartMatch] = []
  if outer.add.is_empty() || inner.add.is_empty() || !ends_with(part, outer.add) {
    return result
  }
  let intermediate = part
    .view(end_offset=part.length() - outer.add.length())
    .to_owned()
  if !condition_matches(outer, intermediate) ||
    !flags_contain(config, inner.continuation, outer.flag) ||
    !ends_with(intermediate.view(), inner.add) {
    return result
  }
  let stem = intermediate
    .view(end_offset=intermediate.length() - inner.add.length())
    .to_owned() +
    inner.strip
  if !condition_matches(inner, stem) {
    return result
  }
  for entry in dictionary.find_entries(stem) {
    if flags_contain(config, entry.flags, inner.flag) &&
      entry_can_be_compound(config, entry) {
      result.push({
        flags: entry.flags + inner.continuation + outer.continuation,
        prefix: false,
        suffix: true,
        prefix_permit: false,
        suffix_permit: affix_compound_permit(config, outer),
        forbid: affix_compound_forbid(config, outer),
      })
    }
  }
  result
}

///|
fn compound_part_matches(
  dictionary : Dictionary,
  config : AffixConfig,
  part : StringView,
) -> Array[CompoundPartMatch] {
  let result = exact_part_matches(dictionary, config, part.to_owned())
  for rule in config.prefixes {
    for item in prefix_part_matches(dictionary, config, part, rule) {
      result.push(item)
    }
  }
  for rule in config.suffixes {
    for item in suffix_part_matches(dictionary, config, part, rule) {
      result.push(item)
    }
  }
  for outer in config.suffixes {
    for inner in config.suffixes {
      for
        item in twofold_suffix_part_matches(
          dictionary, config, part, outer, inner,
        ) {
        result.push(item)
      }
    }
  }
  for prefix in config.prefixes {
    for suffix in config.suffixes {
      for item in cross_part_matches(dictionary, config, part, prefix, suffix) {
        result.push(item)
      }
    }
  }
  result
}

///|
fn compound_boundary_has_triple(word : StringView, position : Int) -> Bool {
  if position <= 0 || position >= word.length() {
    return false
  }
  let left = word.unsafe_get(position - 1)
  let right = word.unsafe_get(position)
  if left != right {
    return false
  }
  (position > 1 && word.unsafe_get(position - 2) == left) ||
  (position + 1 < word.length() && word.unsafe_get(position + 1) == right)
}

///|
fn compound_boundary_has_bad_case(word : StringView, position : Int) -> Bool {
  if position <= 0 || position >= word.length() {
    return false
  }
  let left_code = word.unsafe_get(position - 1)
  let right_code = word.unsafe_get(position)
  if left_code.to_int() == 45 || right_code.to_int() == 45 {
    return false
  }
  match (left_code.to_char(), right_code.to_char()) {
    (Some(left), Some(right)) =>
      is_uppercase_char(left) || is_uppercase_char(right)
    _ => false
  }
}

///|
fn compound_role_matches(
  config : AffixConfig,
  flags : String,
  is_first : Bool,
  is_last : Bool,
) -> Bool {
  match config.compound_flag {
    Some(flag) => if flags_contain(config, flags, flag) { return true }
    None => ()
  }
  if is_first {
    match config.compound_begin {
      Some(flag) => flags_contain(config, flags, flag)
      None => false
    }
  } else if is_last {
    match config.compound_end {
      Some(flag) => flags_contain(config, flags, flag)
      None => false
    }
  } else {
    match config.compound_middle {
      Some(flag) => flags_contain(config, flags, flag)
      None => false
    }
  }
}

///|

///|

///|
fn part_forbidden_by_zero_pattern(
  config : AffixConfig,
  part : CompoundPartMatch,
) -> Bool {
  for pattern in config.compound_patterns {
    if pattern.end_zero {
      match pattern.end_flag {
        Some(flag) => if flags_contain(config, part.flags, flag) { return true }
        None => ()
      }
    }
  }
  false
}

///|
fn compound_pattern_matches(
  config : AffixConfig,
  pattern : CompoundPattern,
  left_part : String,
  left : CompoundPartMatch,
  right_part : StringView,
  right : CompoundPartMatch,
) -> Bool {
  if pattern.end_zero && (left.prefix || left.suffix) {
    return false
  }
  if !pattern.end_zero &&
    !pattern.end_chars.is_empty() &&
    !ends_with(left_part.view(), pattern.end_chars) {
    return false
  }
  match pattern.end_flag {
    Some(flag) => if !flags_contain(config, left.flags, flag) { return false }
    None => ()
  }
  if !pattern.begin_chars.is_empty() &&
    right_part.find(pattern.begin_chars) != Some(0) {
    return false
  }
  match pattern.begin_flag {
    Some(flag) => if !flags_contain(config, right.flags, flag) { return false }
    None => ()
  }
  true
}

///|
fn compound_boundary_forbidden_by_pattern(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  left_part : String,
  left : CompoundPartMatch,
  boundary : Int,
) -> Bool {
  let mut end = boundary + 1
  while end <= word.length() {
    let right_view = word.view(start_offset=boundary, end_offset=end)
    if dictionary.find_entries(left_part + " " + right_view.to_owned()).length() >
      0 {
      return true
    }
    for right in compound_part_matches(dictionary, config, right_view) {
      for pattern in config.compound_patterns {
        if compound_pattern_matches(
            config, pattern, left_part, left, right_view, right,
          ) {
          return true
        }
      }
    }
    end += 1
  }
  false
}

///|
fn compound_part_length_ok(part : StringView, config : AffixConfig) -> Bool {
  part.length() >= config.compound_min ||
  (part.length() == 1 && part.unsafe_get(0).to_int() == 45)
}

///|
fn compound_boundary_has_triple_chars(
  chars : Array[Char],
  position : Int,
) -> Bool {
  if position <= 0 || position >= chars.length() {
    return false
  }
  let left = chars[position - 1]
  let right = chars[position]
  if left != right {
    return false
  }
  (position > 1 && chars[position - 2] == left) ||
  (position + 1 < chars.length() && chars[position + 1] == right)
}

///|
fn compound_boundary_has_bad_case_chars(
  chars : Array[Char],
  position : Int,
) -> Bool {
  if position <= 0 || position >= chars.length() {
    return false
  }
  let left = chars[position - 1]
  let right = chars[position]
  if left.to_int() == 45 || right.to_int() == 45 {
    return false
  }
  is_uppercase_char(left) || is_uppercase_char(right)
}

///|
fn compound_part_length_ok_chars(length : Int, config : AffixConfig) -> Bool {
  length >= config.compound_min
}

///|
fn compound_search_chars(
  dictionary : Dictionary,
  config : AffixConfig,
  chars : Array[Char],
  start : Int,
  part_count : Int,
  previous_part : String,
  max_parts : Int,
) -> Bool {
  if start >= chars.length() || part_count >= max_parts {
    return false
  }
  let mut end = start + 1
  while end <= chars.length() {
    let is_first = start == 0
    let is_last = end == chars.length()
    if !compound_part_length_ok_chars(end - start, config) {
      end += 1
      continue
    }
    let part = String::from_array(chars.exact_view(start~, end~))
    let part_view = part.view()
    let part_matches = compound_part_matches(dictionary, config, part_view)
    let mut part_forbidden = false
    if !is_last {
      for candidate in part_matches {
        if candidate.forbid {
          part_forbidden = true
          break
        }
      }
    }
    if part_forbidden {
      end += 1
      continue
    }
    for item in part_matches {
      if !compound_role_matches(config, item.flags, is_first, is_last) {
        continue
      }
      if !is_first && item.prefix && !item.prefix_permit {
        continue
      }
      if !is_last && item.suffix && !item.suffix_permit {
        continue
      }
      if is_last && item.suffix && part_forbidden_by_zero_pattern(config, item) {
        continue
      }
      if !is_last {
        let mut right_end = end + 1
        let mut forbidden = false
        while right_end <= chars.length() {
          let right = String::from_array(
            chars.exact_view(start=end, end=right_end),
          )
          if dictionary.find_entries(part + " " + right).length() > 0 {
            forbidden = true
            break
          }
          for
            right_match in compound_part_matches(
              dictionary,
              config,
              right.view(),
            ) {
            for pattern in config.compound_patterns {
              if compound_pattern_matches(
                  config,
                  pattern,
                  part,
                  item,
                  right.view(),
                  right_match,
                ) {
                forbidden = true
                break
              }
            }
            if forbidden {
              break
            }
          }
          if forbidden {
            break
          }
          right_end += 1
        }
        if forbidden {
          continue
        }
      }
      if config.check_compound_dup && is_last && previous_part == part {
        continue
      }
      if config.check_compound_triple &&
        compound_boundary_has_triple_chars(chars, end) {
        continue
      }
      if config.check_compound_case &&
        compound_boundary_has_bad_case_chars(chars, end) {
        continue
      }
      if is_last {
        if part_count >= 1 {
          return true
        }
      } else if compound_search_chars(
          dictionary,
          config,
          chars,
          end,
          part_count + 1,
          part,
          max_parts,
        ) {
        return true
      }
    }
    end += 1
  }
  false
}

///|
fn compound_search(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  start : Int,
  part_count : Int,
  previous_part : String,
  max_parts : Int,
) -> Bool {
  if start >= word.length() || part_count >= max_parts {
    return false
  }
  let mut end = start + 1
  while end <= word.length() {
    let is_first = start == 0
    let is_last = end == word.length()
    let part_view = word.view(start_offset=start, end_offset=end)
    if !compound_part_length_ok(part_view, config) {
      end += 1
      continue
    }
    let part = part_view.to_owned()
    let part_matches = compound_part_matches(dictionary, config, part_view)
    let mut part_forbidden = false
    if !is_last {
      for candidate in part_matches {
        if candidate.forbid {
          part_forbidden = true
          break
        }
      }
    }
    if part_forbidden {
      end += 1
      continue
    }
    for item in part_matches {
      let role_ok = compound_role_matches(config, item.flags, is_first, is_last)
      if !role_ok {
        continue
      }
      if !is_first && item.prefix && !item.prefix_permit {
        continue
      }
      if !is_last && item.suffix && !item.suffix_permit {
        continue
      }
      if is_last && item.suffix && part_forbidden_by_zero_pattern(config, item) {
        continue
      }
      if !is_last &&
        compound_boundary_forbidden_by_pattern(
          dictionary, config, word, part, item, end,
        ) {
        continue
      }
      if config.check_compound_dup && is_last && previous_part == part {
        continue
      }
      if config.check_compound_triple && compound_boundary_has_triple(word, end) {
        continue
      }
      if config.check_compound_case && compound_boundary_has_bad_case(word, end) {
        continue
      }
      if is_last {
        if part_count >= 1 {
          return true
        }
      } else if compound_search(
          dictionary,
          config,
          word,
          end,
          part_count + 1,
          part,
          max_parts,
        ) {
        return true
      }
    }
    end += 1
  }
  false
}

///|
priv struct CompoundRuleToken {
  flag : String
  quantifier : String
}

///|
fn parse_compound_rule(
  pattern : String,
  flag_mode : String,
) -> Array[CompoundRuleToken] {
  let chars = pattern.to_array()
  let result : Array[CompoundRuleToken] = []
  let mut index = 0
  while index < chars.length() {
    let flag = if chars[index] == '(' {
      let mut close = index + 1
      while close < chars.length() && chars[close] != ')' {
        close += 1
      }
      if close >= chars.length() {
        return []
      }
      let value = pattern
        .view(start_offset=index + 1, end_offset=close)
        .to_owned()
      index = close + 1
      value
    } else if flag_mode == "long" && index + 1 < chars.length() {
      let value = chars[index].to_string() + chars[index + 1].to_string()
      index += 2
      value
    } else {
      let value = chars[index].to_string()
      index += 1
      value
    }
    let quantifier = if index < chars.length() &&
      (chars[index] == '*' || chars[index] == '?') {
      let value = chars[index].to_string()
      index += 1
      value
    } else {
      ""
    }
    result.push({ flag, quantifier, })
  }
  result
}

///|
fn compound_rule_match_at(
  tokens : Array[CompoundRuleToken],
  flags : Array[String],
  token_index : Int,
  part_index : Int,
  config : AffixConfig,
) -> Bool {
  if token_index >= tokens.length() {
    return part_index >= flags.length()
  }
  let token = tokens[token_index]
  let can_consume = part_index < flags.length() &&
    flags_contain(config, flags[part_index], token.flag)
  match token.quantifier {
    "" =>
      can_consume &&
      compound_rule_match_at(
        tokens,
        flags,
        token_index + 1,
        part_index + 1,
        config,
      )
    "?" =>
      compound_rule_match_at(tokens, flags, token_index + 1, part_index, config) ||
      (
        can_consume &&
        compound_rule_match_at(
          tokens,
          flags,
          token_index + 1,
          part_index + 1,
          config,
        )
      )
    "*" =>
      compound_rule_match_at(tokens, flags, token_index + 1, part_index, config) ||
      (
        can_consume &&
        compound_rule_match_at(
          tokens,
          flags,
          token_index,
          part_index + 1,
          config,
        )
      )
    _ => false
  }
}

///|
fn compound_rule_matches(
  pattern : String,
  flags : Array[String],
  config : AffixConfig,
) -> Bool {
  if flags.length() == 0 {
    return false
  }
  if flags.length() == 1 {
    match config.only_in_compound {
      Some(flag) => if flags_contain(config, flags[0], flag) { return false }
      None => ()
    }
  }
  let tokens = parse_compound_rule(pattern, config.flag_mode)
  if tokens.length() == 0 {
    return false
  }
  compound_rule_match_at(tokens, flags, 0, 0, config)
}

///|
fn compound_rule_search(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  pattern : String,
  start : Int,
  flags : Array[String],
  max_parts : Int,
) -> Bool {
  if start >= word.length() || flags.length() >= max_parts {
    return false
  }
  let mut end = start + 1
  while end <= word.length() {
    let is_last = end == word.length()
    let part_view = word.view(start_offset=start, end_offset=end)
    if compound_part_length_ok(part_view, config) {
      for item in compound_part_matches(dictionary, config, part_view) {
        if !is_last && (item.prefix || item.suffix) {
          continue
        }
        let next_flags = flags.copy()
        next_flags.push(item.flags)
        if is_last {
          if compound_rule_matches(pattern, next_flags, config) {
            return true
          }
        } else if compound_rule_search(
            dictionary, config, word, pattern, end, next_flags, max_parts,
          ) {
          return true
        }
      }
    }
    end += 1
  }
  false
}

///|
fn compound_rule_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  if config.compound_rules.length() == 0 {
    return false
  }
  for pattern in config.compound_rules {
    if compound_rule_search(dictionary, config, word.view(), pattern, 0, [], 32) {
      return true
    }
  }
  false
}

///|
fn compound_rep_correctable(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  for rule in config.replacements {
    match replacement_candidate(word, rule.from, rule.to) {
      Some(candidate) => {
        if dictionary.find_entries(candidate).length() > 0 {
          return true
        }
        for entry in dictionary.entries {
          if entry.word.length() >= config.compound_min &&
            candidate.find(entry.word) is Some(_) &&
            word.find(entry.word) is None {
            return true
          }
        }
      }
      None => ()
    }
  }
  false
}

///|
fn find_substring_from(text : String, needle : String, start : Int) -> Int? {
  if start > text.length() || needle.is_empty() {
    None
  } else {
    match text.view(start_offset=start).find(needle) {
      Some(index) => Some(start + index)
      None => None
    }
  }
}

///|
fn replace_range(
  text : String,
  start : Int,
  length : Int,
  replacement : String,
) -> String {
  text.view(end_offset=start).to_owned() +
  replacement +
  text.view(start_offset=start + length).to_owned()
}

///|
fn two_part_compound_allows(
  dictionary : Dictionary,
  config : AffixConfig,
  left : String,
  right : String,
) -> Bool {
  if left.char_length() < config.compound_min ||
    right.char_length() < config.compound_min {
    return false
  }
  for left_match in compound_part_matches(dictionary, config, left.view()) {
    if !compound_role_matches(config, left_match.flags, true, false) {
      continue
    }
    if left_match.suffix && !left_match.suffix_permit {
      continue
    }
    for right_match in compound_part_matches(dictionary, config, right.view()) {
      if !compound_role_matches(config, right_match.flags, false, true) {
        continue
      }
      if right_match.prefix && !right_match.prefix_permit {
        continue
      }
      return true
    }
  }
  false
}

///|
fn compound_pattern_replacement_allows(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  for pattern in config.compound_patterns {
    match pattern.replacement {
      Some(replacement) => {
        if replacement.is_empty() {
          continue
        }
        let mut offset = 0
        while offset <= word.length() {
          match find_substring_from(word, replacement, offset) {
            Some(position) => {
              let boundary = position + pattern.end_chars.length()
              let reconstructed = replace_range(
                word,
                position,
                replacement.length(),
                pattern.end_chars + pattern.begin_chars,
              )
              if boundary > 0 && boundary < reconstructed.length() {
                let left = reconstructed.view(end_offset=boundary).to_owned()
                let right = reconstructed.view(start_offset=boundary).to_owned()
                for
                  left_match in compound_part_matches(
                    dictionary,
                    config,
                    left.view(),
                  ) {
                  if !compound_role_matches(
                      config,
                      left_match.flags,
                      true,
                      false,
                    ) {
                    continue
                  }
                  for
                    right_match in compound_part_matches(
                      dictionary,
                      config,
                      right.view(),
                    ) {
                    if !compound_role_matches(
                        config,
                        right_match.flags,
                        false,
                        true,
                      ) {
                      continue
                    }
                    if compound_pattern_matches(
                        config,
                        pattern,
                        left,
                        left_match,
                        right.view(),
                        right_match,
                      ) {
                      return true
                    }
                  }
                }
              }
              offset = position + 1
            }
            None => break
          }
        }
      }
      None => ()
    }
  }
  false
}

///|
fn simplified_triple_allows(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  let chars = word.to_array()
  let mut split = 1
  while split < chars.length() {
    let left = String::from_array(chars.exact_view(end=split))
    let right = String::from_array(chars.exact_view(start=split))
    if !left.is_empty() && !right.is_empty() {
      let left_chars = left.to_array()
      let boundary_char = left_chars[left_chars.length() - 1].to_string()
      let reconstructed_right = boundary_char + right
      if two_part_compound_allows(dictionary, config, left, reconstructed_right) {
        return true
      }
    }
    split += 1
  }
  false
}

///|
fn compound_force_search(
  dictionary : Dictionary,
  config : AffixConfig,
  word : StringView,
  start : Int,
  part_count : Int,
  previous_part : String,
  max_parts : Int,
  found_force : Bool,
) -> Bool {
  if start >= word.length() || part_count >= max_parts {
    return false
  }
  let mut end = start + 1
  while end <= word.length() {
    let is_first = start == 0
    let is_last = end == word.length()
    let part_view = word.view(start_offset=start, end_offset=end)
    if compound_part_length_ok(part_view, config) {
      let part = part_view.to_owned()
      for item in compound_part_matches(dictionary, config, part_view) {
        if !compound_role_matches(config, item.flags, is_first, is_last) {
          continue
        }
        if !is_first && item.prefix && !item.prefix_permit {
          continue
        }
        if !is_last && item.suffix && !item.suffix_permit {
          continue
        }
        if !is_last &&
          compound_boundary_forbidden_by_pattern(
            dictionary, config, word, part, item, end,
          ) {
          continue
        }
        if config.check_compound_dup && is_last && previous_part == part {
          continue
        }
        if config.check_compound_triple &&
          compound_boundary_has_triple(word, end) {
          continue
        }
        if config.check_compound_case &&
          compound_boundary_has_bad_case(word, end) {
          continue
        }
        let item_force = if is_last {
          match config.force_ucase {
            Some(flag) => flags_contain(config, item.flags, flag)
            None => false
          }
        } else {
          false
        }
        let path_has_force = found_force || item_force
        if is_last {
          if part_count >= 1 && path_has_force {
            return true
          }
        } else if compound_force_search(
            dictionary,
            config,
            word,
            end,
            part_count + 1,
            part,
            max_parts,
            path_has_force,
          ) {
          return true
        }
      }
    }
    end += 1
  }
  false
}

///|
fn compound_force_ucase_violation(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  match config.force_ucase {
    Some(_) => {
      let max_parts = if config.compound_word_max > 0 {
        config.compound_word_max
      } else {
        8
      }
      compound_force_search(
        dictionary,
        config,
        word.view(),
        0,
        0,
        "",
        max_parts,
        false,
      )
    }
    None => false
  }
}

///|
fn compound_has_three_exact_parts(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  let chars = word.to_array()
  if chars.length() < config.compound_min * 3 {
    return false
  }
  let mut first_end = config.compound_min
  while first_end < chars.length() {
    let first = String::from_array(chars.exact_view(end=first_end))
    for first_match in exact_part_matches(dictionary, config, first) {
      if !compound_role_matches(config, first_match.flags, true, false) {
        continue
      }
      let mut second_end = first_end + config.compound_min
      while second_end < chars.length() {
        let second = String::from_array(
          chars.exact_view(start=first_end, end=second_end),
        )
        let third = String::from_array(chars.exact_view(start=second_end))
        for second_match in exact_part_matches(dictionary, config, second) {
          if !compound_role_matches(config, second_match.flags, false, false) {
            continue
          }
          for third_match in exact_part_matches(dictionary, config, third) {
            if compound_role_matches(config, third_match.flags, false, true) {
              return true
            }
          }
        }
        second_end += 1
      }
    }
    first_end += 1
  }
  false
}

///|
fn compound_is_typo_of_dictionary_word(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  if !compound_has_three_exact_parts(dictionary, config, word) {
    return false
  }
  for entry in dictionary.entries {
    if !entry_is_forbidden(config, entry) &&
      entry.word != word &&
      levenshtein_distance(word, entry.word) == 1 {
      return true
    }
  }
  false
}

///|
fn compound_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  let max_parts = if config.compound_word_max > 0 {
    config.compound_word_max
  } else {
    8
  }
  if word.length() < config.compound_min * 2 {
    return false
  }
  if compound_pattern_replacement_allows(dictionary, config, word) {
    return true
  }
  if config.simplified_triple &&
    simplified_triple_allows(dictionary, config, word) {
    return true
  }
  let matched = if word.char_length() == word.length() {
    compound_search(dictionary, config, word.view(), 0, 0, "", max_parts)
  } else {
    compound_search_chars(
      dictionary,
      config,
      word.to_array(),
      0,
      0,
      "",
      max_parts,
    )
  }
  matched &&
  !(config.check_compound_rep &&
  compound_rep_correctable(dictionary, config, word)) &&
  !compound_is_typo_of_dictionary_word(dictionary, config, word)
}

///|
fn reverse_suffix_rule(word : String, rule : AffixRule) -> String? {
  if rule.add.is_empty() || !ends_with(word.view(), rule.add) {
    None
  } else {
    let stem = word
      .view(end_offset=word.length() - rule.add.length())
      .to_owned() +
      rule.strip
    if condition_matches(rule, stem) {
      Some(stem)
    } else {
      None
    }
  }
}

///|
fn reverse_prefix_rule(word : String, rule : AffixRule) -> String? {
  if rule.add.is_empty() || word.find(rule.add) != Some(0) {
    None
  } else {
    let stem = rule.strip + word.view(start_offset=rule.add.length()).to_owned()
    if condition_matches(rule, stem) {
      Some(stem)
    } else {
      None
    }
  }
}

///|
fn pending_after_rule(
  config : AffixConfig,
  pending : Array[String],
  rule : AffixRule,
) -> Array[String] {
  let result : Array[String] = []
  for flag in pending {
    if !flags_contain(config, rule.continuation, flag) {
      remember_unique(result, flag)
    }
  }
  remember_unique(result, rule.flag)
  result
}

///|
fn pending_matches_entry(
  config : AffixConfig,
  pending : Array[String],
  entry : DicEntry,
) -> Bool {
  for flag in pending {
    if !flags_contain(config, entry.flags, flag) {
      return false
    }
  }
  true
}

///|
fn affix_chain_search(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
  depth : Int,
  pending : Array[String],
  has_prefix : Bool,
  has_suffix : Bool,
  all_cross : Bool,
  needy_count : Int,
) -> Bool {
  if depth > 4 {
    return false
  }
  if depth > 0 && (needy_count == 0 || needy_count < depth) {
    for entry in dictionary.find_entries(word) {
      if entry_can_be_affixed(config, entry) &&
        pending_matches_entry(config, pending, entry) {
        return true
      }
    }
  }
  for rule in config.suffixes {
    if rule_is_only_in_compound(config, rule) {
      continue
    }
    match reverse_suffix_rule(word, rule) {
      Some(stem) => {
        let cross_ok = all_cross && rule.cross_product
        if has_prefix && !cross_ok {
          continue
        }
        let next_pending = pending_after_rule(config, pending, rule)
        if affix_chain_search(
            dictionary,
            config,
            stem,
            depth + 1,
            next_pending,
            has_prefix,
            true,
            cross_ok,
            needy_count + (if rule_needs_affix(config, rule) { 1 } else { 0 }),
          ) {
          return true
        }
      }
      None => ()
    }
  }
  for rule in config.prefixes {
    if rule_is_only_in_compound(config, rule) {
      continue
    }
    match reverse_prefix_rule(word, rule) {
      Some(stem) => {
        let cross_ok = all_cross && rule.cross_product
        if has_suffix && !cross_ok {
          continue
        }
        let next_pending = pending_after_rule(config, pending, rule)
        if affix_chain_search(
            dictionary,
            config,
            stem,
            depth + 1,
            next_pending,
            true,
            has_suffix,
            cross_ok,
            needy_count + (if rule_needs_affix(config, rule) { 1 } else { 0 }),
          ) {
          return true
        }
      }
      None => ()
    }
  }
  false
}

///|
fn deep_affix_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> SpellResult? {
  if affix_chain_search(dictionary, config, word, 0, [], false, false, true, 0) {
    Some({ accepted: true, stem: Some(word), prefix: None, suffix: None, })
  } else {
    None
  }
}

///|
fn spell_core(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> SpellResult {
  match exact_result(dictionary, config, word) {
    Some(result) => return result
    None => ()
  }
  let view = word.view()
  for rule in config.prefixes {
    match prefix_result(dictionary, config, view, rule) {
      Some(result) => return result
      None => ()
    }
  }
  for rule in config.suffixes {
    match suffix_result(dictionary, config, view, rule) {
      Some(result) => return result
      None => ()
    }
  }
  if config.complex_prefixes {
    for outer in config.prefixes {
      for inner in config.prefixes {
        match twofold_prefix_result(dictionary, config, view, outer, inner) {
          Some(result) => return result
          None => ()
        }
      }
    }
  }
  for outer in config.suffixes {
    for inner in config.suffixes {
      match twofold_suffix_result(dictionary, config, view, outer, inner) {
        Some(result) => return result
        None => ()
      }
    }
  }
  for prefix in config.prefixes {
    for suffix in config.suffixes {
      match cross_result(dictionary, config, view, prefix, suffix) {
        Some(result) => return result
        None => ()
      }
    }
  }
  match deep_affix_result(dictionary, config, word) {
    Some(result) => return result
    None => ()
  }
  if compound_result(dictionary, config, word) {
    return { accepted: true, stem: None, prefix: None, suffix: None, }
  }
  if compound_rule_result(dictionary, config, word) {
    return { accepted: true, stem: None, prefix: None, suffix: None, }
  }
  { accepted: false, stem: None, prefix: None, suffix: None, }
}

///|
fn ascii_lower(text : String) -> String {
  let result = StringBuilder()
  for c in text {
    result.write_char(@unicode.to_lowercase(c))
  }
  result.to_string()
}

///|
fn language_lower(config : AffixConfig, text : String) -> String {
  let result = StringBuilder()
  for c in text {
    if config.language == "tr" && c == 'I' {
      result.write_char('ı')
    } else if config.language == "tr" && c == 'İ' {
      result.write_char('i')
    } else {
      result.write_char(@unicode.to_lowercase(c))
    }
  }
  result.to_string()
}

///|
fn sharp_case_candidates(text : String) -> Array[String] {
  let lower = ascii_lower(text)
  let chars = lower.to_array()
  let mut occurrences = 0
  let mut i = 0
  while i + 1 < chars.length() {
    if chars[i] == 's' && chars[i + 1] == 's' {
      occurrences += 1
      i += 2
    } else {
      i += 1
    }
  }
  if occurrences > 8 {
    occurrences = 8
  }
  let result : Array[String] = []
  let combinations = 1 << occurrences
  for mask in 0..> bit) & 1) == 1 {
          builder.write_char('ß')
        } else {
          builder.write_char('s')
          builder.write_char('s')
        }
        index += 2
        bit += 1
      } else {
        builder.write_char(chars[index])
        index += 1
      }
    }
    let variant = builder.to_string()
    if !result.contains(variant) {
      result.push(variant)
    }
    let capitalized = capitalize_ascii(variant)
    if !result.contains(capitalized) {
      result.push(capitalized)
    }
  }
  result
}

///|
fn unicode_upper_char(c : Char) -> Char {
  let code = c.to_int()
  if code >= 97 && code <= 122 {
    (code - 32).unsafe_to_char()
  } else if code >= 0xE0 && code <= 0xFE && code != 0xF7 {
    (code - 32).unsafe_to_char()
  } else if code == 0xFF {
    '\u{0178}'
  } else if code == 0x0153 {
    '\u{0152}'
  } else if code >= 0x0101 && code <= 0x017F && code % 2 == 1 {
    (code - 1).unsafe_to_char()
  } else {
    c
  }
}

///|
fn ascii_upper_char(c : Char) -> Char {
  if c >= 'a' && c <= 'z' {
    (c.to_int() - 32).unsafe_to_char()
  } else {
    c
  }
}

///|
fn capitalize_ascii(text : String) -> String {
  let chars = text.to_array()
  if chars.length() == 0 {
    return text
  }
  let result = StringBuilder()
  result.write_char(ascii_upper_char(chars[0]))
  for i in 1.. Bool {
  if (c >= 'A' && c <= 'Z') || c == '\u{0130}' {
    true
  } else {
    let lowered = @unicode.to_lowercase(c)
    lowered != c
  }
}

///|
fn is_probably_lowercase_char(c : Char) -> Bool {
  if c >= 'a' && c <= 'z' {
    true
  } else {
    let lower = @unicode.to_lowercase(c)
    lower == c &&
    c.to_int() > 127 &&
    c != '.' &&
    c != '\'' &&
    c != '-' &&
    c != '_'
  }
}

///|
fn is_all_upper(text : String) -> Bool {
  let mut has_letter = false
  let mut valid = true
  for c in text {
    if is_uppercase_char(c) {
      has_letter = true
    } else if is_probably_lowercase_char(c) {
      valid = false
      break
    }
  }
  has_letter && valid
}

///|
fn is_capitalized(text : String) -> Bool {
  let chars = text.to_array()
  if chars.length() == 0 || !is_uppercase_char(chars[0]) {
    return false
  }
  for i in 1.. String {
  let mut end = text.length()
  while end > 0 && text.unsafe_get(end - 1) == '.' {
    end -= 1
  }
  if end == text.length() {
    text
  } else {
    text.view(end_offset=end).to_owned()
  }
}

///|
fn has_exact_forbidden(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  let mut found_forbidden = false
  for entry in dictionary.find_entries(word) {
    if entry_is_forbidden(config, entry) {
      found_forbidden = true
    } else {
      return false
    }
  }
  found_forbidden
}

///|
fn casefold_keepcase(config : AffixConfig, text : String) -> String {
  language_lower(config, text).replace_all(old="\u{00DF}", new="ss")
}

///|
fn result_has_forbidden_homonym(
  dictionary : Dictionary,
  config : AffixConfig,
  result : SpellResult,
) -> Bool {
  match result.stem {
    Some(stem) => {
      let lower_stem = casefold_keepcase(config, stem)
      for entry in dictionary.entries {
        if casefold_keepcase(config, entry.word) == lower_stem &&
          entry_is_forbidden(config, entry) {
          return true
        }
      }
      false
    }
    None => false
  }
}

///|
fn result_is_keepcase_only(
  dictionary : Dictionary,
  config : AffixConfig,
  result : SpellResult,
) -> Bool {
  match result.stem {
    Some(stem) => {
      let mut has_match = false
      let mut has_non_keepcase = false
      let lower_stem = casefold_keepcase(config, stem)
      for entry in dictionary.entries {
        if casefold_keepcase(config, entry.word) == lower_stem {
          has_match = true
          if !entry_keeps_case(config, entry) {
            has_non_keepcase = true
          }
        }
      }
      has_match && !has_non_keepcase
    }
    None => false
  }
}

///|
fn is_numeric_word(config : AffixConfig, word : String) -> Bool {
  if !config.word_chars.contains("0") && !config.word_chars.contains("1") {
    return false
  }
  let chars = word.to_array()
  if chars.length() == 0 {
    return false
  }
  let mut digits = 0
  let mut separators = 0
  for i in 0..= '0' && ch <= '9' {
      digits += 1
    } else if ch == '.' || ch == ',' || ch == '-' {
      if i == 0 || i + 1 >= chars.length() {
        return false
      }
      let before = chars[i - 1]
      let after = chars[i + 1]
      if before < '0' || before > '9' || after < '0' || after > '9' {
        return false
      }
      separators += 1
      if separators > 1 {
        return false
      }
    } else {
      return false
    }
  }
  digits > 0
}

///|
fn forbidden_prefix_extension(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  if config.forbidden_word is None {
    return false
  }
  for entry in dictionary.entries {
    if entry_is_forbidden(config, entry) &&
      word.length() > entry.word.length() &&
      word.find(entry.word) == Some(0) {
      return true
    }
  }
  false
}

///|
fn phrase_is_accepted(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  if !word.contains(" ") {
    return false
  }
  let mut found = false
  for part in word.split(" ") {
    let token = part.to_owned()
    if token.is_empty() {
      return false
    }
    found = true
    if !spell(dictionary, config, token).accepted {
      return false
    }
  }
  found
}

///|
fn apply_ignore(config : AffixConfig, word : String) -> String {
  if config.ignore_chars.is_empty() {
    return word
  }
  let ignored = config.ignore_chars.to_array()
  let result = StringBuilder()
  for c in word {
    if !ignored.contains(c) {
      result.write_char(c)
    }
  }
  result.to_string()
}

///|
fn normalized_dictionary(
  config : AffixConfig,
  dictionary : Dictionary,
) -> Dictionary {
  if config.ignore_chars.is_empty() {
    return dictionary
  }
  let entries : Array[DicEntry] = []
  let index : Map[String, Array[DicEntry]] = Map([])
  for entry in dictionary.entries {
    let normalized = apply_ignore(config, entry.word)
    let item = if normalized == entry.word {
      entry
    } else {
      { word: normalized, flags: entry.flags, morph: entry.morph, }
    }
    entries.push(item)
    match index.get(item.word) {
      Some(bucket) => bucket.push(item)
      None => index.set(item.word, [item])
    }
  }
  { declared_count: dictionary.declared_count, entries, index, }
}

///|
fn spell_breakable(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  if !config.break_enabled || config.break_patterns.length() == 0 {
    return false
  }
  for position in 0.. matched_length && suffix.find(pattern) == Some(0) {
        matched_length = pattern.length()
      }
    }
    if matched_length > 0 {
      let left = word.view(end_offset=position).to_owned()
      let right = word.view(start_offset=position + matched_length).to_owned()
      if config.break_explicit && (left.is_empty() || right.is_empty()) {
        continue
      }
      let left_ok = left.is_empty() || spell(dictionary, config, left).accepted
      let right_ok = right.is_empty() ||
        spell(dictionary, config, right).accepted
      if left_ok && right_ok && (!left.is_empty() || !right.is_empty()) {
        return true
      }
    }
  }
  false
}

///|
fn apply_conversions(rules : Array[ReplacementRule], word : String) -> String {
  let mut result = word
  let mut iteration = 0
  while iteration < 100 {
    iteration += 1
    let mut best_rule : Int = -1
    let mut best_position = result.length() + 1
    let mut best_length = 0
    for i in 0.. {
          let length = rules[i].from.length()
          if position < best_position ||
            (position == best_position && length > best_length) {
            best_rule = i
            best_position = position
            best_length = length
          }
        }
        None => ()
      }
    }
    if best_rule < 0 {
      break
    }
    let rule = rules[best_rule]
    match replace_first(result, rule.from, rule.to) {
      Some(value) => {
        if value == result {
          break
        }
        result = value
      }
      None => break
    }
  }
  result
}

///|
fn apply_iconv(config : AffixConfig, word : String) -> String {
  apply_conversions(config.iconv_rules, word)
}

///|
fn dotted_i_case_equal(left : String, right : String) -> Bool {
  if !left.contains("\u{0130}") || !right.contains("\u{0130}") {
    false
  } else {
    ascii_lower(left.replace_all(old="\u{0130}", new="i")) ==
    ascii_lower(right.replace_all(old="\u{0130}", new="i"))
  }
}

///|
fn contains_keepcase_part(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> Bool {
  for entry in dictionary.entries {
    if entry_keeps_case(config, entry) && word.contains(entry.word) {
      return true
    }
  }
  false
}

///|
fn case_variant_result(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> SpellResult? {
  if is_all_upper(word) {
    let lower_word = language_lower(config, word)
    for candidate in generate_candidates(dictionary, config) {
      if (
          language_lower(config, candidate) == lower_word ||
          dotted_i_case_equal(candidate, word)
        ) &&
        !has_exact_forbidden(dictionary, config, candidate) {
        let result = spell_core(dictionary, config, candidate)
        if result.accepted &&
          !result_is_keepcase_only(dictionary, config, result) {
          return Some(result)
        }
      }
    }
  }
  if config.check_sharps &&
    (is_all_upper(word) || is_capitalized(word)) &&
    !word.contains("ss") {
    for candidate in sharp_case_candidates(word) {
      if candidate != word {
        let result = spell_core(dictionary, config, candidate)
        if result.accepted {
          return Some(result)
        }
      }
    }
  }
  if is_all_upper(word) || is_capitalized(word) {
    let lower = language_lower(config, word)
    if lower != word {
      let result = spell_core(dictionary, config, lower)
      if result.accepted &&
        !result_is_keepcase_only(dictionary, config, result) &&
        !result_has_forbidden_homonym(dictionary, config, result) &&
        !(result.stem is None &&
        contains_keepcase_part(dictionary, config, lower)) {
        return Some(result)
      }
    }
    let capitalized = capitalize_ascii(lower)
    if capitalized != word {
      let result = spell_core(dictionary, config, capitalized)
      if result.accepted &&
        !result_is_keepcase_only(dictionary, config, result) &&
        !result_has_forbidden_homonym(dictionary, config, result) {
        return Some(result)
      }
    }
  }
  None
}

///|
pub fn spell(
  dictionary : Dictionary,
  config : AffixConfig,
  word : String,
) -> SpellResult {
  let word = apply_iconv(config, apply_ignore(config, word))
  let dictionary = normalized_dictionary(config, dictionary)
  if !is_capitalized(word) &&
    !is_all_upper(word) &&
    compound_force_ucase_violation(dictionary, config, word) {
    return { accepted: false, stem: None, prefix: None, suffix: None, }
  }
  if is_numeric_word(config, word) {
    return { accepted: true, stem: Some(word), prefix: None, suffix: None, }
  }
  if has_exact_forbidden(dictionary, config, word) {
    return { accepted: false, stem: None, prefix: None, suffix: None, }
  }
  if forbidden_prefix_extension(dictionary, config, word) {
    return { accepted: false, stem: None, prefix: None, suffix: None, }
  }
  if phrase_is_accepted(dictionary, config, word) {
    return { accepted: true, stem: None, prefix: None, suffix: None, }
  }
  let direct = spell_core(dictionary, config, word)
  if direct.accepted {
    return direct
  }
  if spell_breakable(dictionary, config, word) {
    return { accepted: true, stem: None, prefix: None, suffix: None, }
  }
  let without_periods = strip_trailing_periods(word)
  if without_periods != word {
    let bare = spell_core(dictionary, config, without_periods)
    if bare.accepted {
      return bare
    }
    match case_variant_result(dictionary, config, without_periods) {
      Some(result) => return result
      None => ()
    }
  }
  match case_variant_result(dictionary, config, word) {
    Some(result) => return result
    None => ()
  }
  direct
}