///|
pub(all) struct SlugOptions {
  separator : Char
  lowercase : Bool
  trim_separator : Bool
  max_length : Int?
  ascii_only : Bool
  fallback : String?
}

///|
pub struct RuleSet {
  raw_word_map : Array[(String, String)]
  raw_char_map : Array[(Char, String)]
  raw_drop_chars : Array[Char]
  raw_separator_chars : Array[Char]
  word_map_buckets : Buckets[(Array[Char], String)]
  char_map_buckets : Buckets[(Char, String)]
  drop_buckets : Buckets[Char]
  separator_buckets : Buckets[Char]
}

///|
pub impl Default for SlugOptions with default() {
  {
    separator: '-',
    lowercase: true,
    trim_separator: true,
    max_length: None,
    ascii_only: true,
    fallback: None,
  }
}

///|
pub impl Default for RuleSet with default() {
  default_rule_set
}

///|
pub fn RuleSet::new(
  word_map? : Array[(String, String)],
  char_map? : Array[(Char, String)],
  drop_chars? : Array[Char],
  separator_chars? : Array[Char],
) -> RuleSet {
  let resolved_word_map = if word_map is Some(value) {
    normalize_word_map(value)
  } else {
    default_word_map
  }
  let resolved_char_map = if char_map is Some(value) {
    dedupe_char_map(value)
  } else {
    default_char_map()
  }
  let resolved_drop_chars = if drop_chars is Some(value) {
    dedupe_chars(value)
  } else {
    default_drop_chars
  }
  let resolved_separator_chars = if separator_chars is Some(value) {
    dedupe_chars(value)
  } else {
    default_separator_chars
  }
  build_ruleset(
    resolved_word_map, resolved_char_map, resolved_drop_chars, resolved_separator_chars,
  )
}

///|
pub fn RuleSet::extend(
  self : RuleSet,
  word_map? : Array[(String, String)] = [],
  char_map? : Array[(Char, String)] = [],
  drop_chars? : Array[Char] = [],
  separator_chars? : Array[Char] = [],
) -> RuleSet {
  build_ruleset(
    merge_word_map(self.raw_word_map, normalize_word_map(word_map)),
    merge_char_map(self.raw_char_map, char_map),
    union_chars(self.raw_drop_chars, drop_chars),
    union_chars(self.raw_separator_chars, separator_chars),
  )
}

///|
pub fn RuleSet::merge(self : RuleSet, other : RuleSet) -> RuleSet {
  build_ruleset(
    merge_word_map(self.raw_word_map, other.raw_word_map),
    merge_char_map(self.raw_char_map, other.raw_char_map),
    union_chars(self.raw_drop_chars, other.raw_drop_chars),
    union_chars(self.raw_separator_chars, other.raw_separator_chars),
  )
}

///|
let default_rule_set : RuleSet = RuleSet::new()

///|
pub fn slugify(
  input : String,
  options? : SlugOptions = SlugOptions::default(),
  rules? : RuleSet = RuleSet::default(),
) -> String {
  let normalized_input = normalize_for_slug(input)
  let expanded_input = apply_word_map(normalized_input, rules)
  let output : Array[Char] = []
  let mut last_is_separator = false

  for c in expanded_input {
    if rules.drop_buckets.contains(c) {
      continue
    }
    if c.is_ascii_alphabetic() || c.is_ascii_digit() {
      output.push(normalize_ascii(c, options.lowercase))
      last_is_separator = false
      continue
    }
    if rules.char_map_buckets.lookup_pair(c) is Some(mapped) {
      append_mapped(
        mapped,
        output,
        options.lowercase,
        options.separator,
        rules,
        last_is_separator,
      )
      last_is_separator = !output.is_empty() &&
        output[output.length() - 1] == options.separator
      continue
    }
    if is_separator_candidate(c, rules) {
      if !output.is_empty() && !last_is_separator {
        output.push(options.separator)
        last_is_separator = true
      }
      continue
    }
    if !options.ascii_only {
      output.push(c)
      last_is_separator = false
    }
  }

  let normalized = if options.trim_separator {
    trim_separator_edges(output, options.separator)
  } else {
    output
  }
  let limited = apply_max_length(normalized, options.max_length)
  let final_chars = if options.trim_separator {
    trim_separator_edges(limited, options.separator)
  } else {
    limited
  }

  if final_chars.length() == 0 {
    if options.fallback is Some(fallback) {
      fallback
    } else {
      ""
    }
  } else {
    [..final_chars]
  }
}

///|
fn build_ruleset(
  raw_word_map : Array[(String, String)],
  raw_char_map : Array[(Char, String)],
  raw_drop_chars : Array[Char],
  raw_separator_chars : Array[Char],
) -> RuleSet {
  let word_map_buckets = Buckets::new_word(257)
  for pair in raw_word_map {
    let (from, to) = pair
    let key_chars : Array[Char] = [..from]
    if !key_chars.is_empty() {
      word_map_buckets[key_chars[0]].push((key_chars, to))
    }
  }

  let char_map_buckets = Buckets::new_pair(257)
  for pair in raw_char_map {
    char_map_buckets[pair.0].push(pair)
  }

  let drop_buckets = Buckets::new_char(64)
  for c in raw_drop_chars {
    drop_buckets[c].push(c)
  }

  let separator_buckets = Buckets::new_char(64)
  for c in raw_separator_chars {
    separator_buckets[c].push(c)
  }

  {
    raw_word_map,
    raw_char_map,
    raw_drop_chars,
    raw_separator_chars,
    word_map_buckets,
    char_map_buckets,
    drop_buckets,
    separator_buckets,
  }
}

///|
fn merge_char_map(
  left : Array[(Char, String)],
  right : Array[(Char, String)],
) -> Array[(Char, String)] {
  let merged : Array[(Char, String)] = [..left]
  for pair in right {
    let (key, value) = pair
    upsert_pair(merged, key, value)
  }
  merged
}

///|
fn merge_word_map(
  left : Array[(String, String)],
  right : Array[(String, String)],
) -> Array[(String, String)] {
  let merged : Array[(String, String)] = [..left]
  for pair in right {
    let (key, value) = pair
    upsert_word_pair(merged, key, value)
  }
  merged
}

///|
fn upsert_pair(
  entries : Array[(Char, String)],
  key : Char,
  value : String,
) -> Unit {
  for i in 0.. Unit {
  for i in 0.. Array[Char] {
  let merged : Array[Char] = [..left]
  for c in right {
    if !merged.contains(c) {
      merged.push(c)
    }
  }
  merged
}

///|
fn dedupe_char_map(entries : Array[(Char, String)]) -> Array[(Char, String)] {
  let deduped : Array[(Char, String)] = []
  for pair in entries {
    let (key, value) = pair
    upsert_pair(deduped, key, value)
  }
  deduped
}

///|
fn normalize_word_map(
  entries : Array[(String, String)],
) -> Array[(String, String)] {
  let deduped : Array[(String, String)] = []
  for pair in entries {
    let (from, to) = pair
    let normalized_key = normalize_for_slug(from)
    if normalized_key != "" {
      upsert_word_pair(deduped, normalized_key, to)
    }
  }
  deduped
}

///|
fn dedupe_chars(chars : Array[Char]) -> Array[Char] {
  let seen = Set::new()
  let deduped = []
  for c in chars {
    if !seen.contains(c) {
      seen.add(c)
      deduped.push(c)
    }
  }
  deduped
}

///|
fn default_char_map() -> Array[(Char, String)] {
  [
    ('\u{00E0}', "a"),
    ('\u{00E1}', "a"),
    ('\u{00E2}', "a"),
    ('\u{00E3}', "a"),
    ('\u{00E4}', "a"),
    ('\u{00E5}', "a"),
    ('\u{00E7}', "c"),
    ('\u{00E8}', "e"),
    ('\u{00E9}', "e"),
    ('\u{00EA}', "e"),
    ('\u{00EB}', "e"),
    ('\u{00EC}', "i"),
    ('\u{00ED}', "i"),
    ('\u{00EE}', "i"),
    ('\u{00EF}', "i"),
    ('\u{00F1}', "n"),
    ('\u{00F2}', "o"),
    ('\u{00F3}', "o"),
    ('\u{00F4}', "o"),
    ('\u{00F5}', "o"),
    ('\u{00F6}', "o"),
    ('\u{00F9}', "u"),
    ('\u{00FA}', "u"),
    ('\u{00FB}', "u"),
    ('\u{00FC}', "u"),
    ('\u{00FD}', "y"),
    ('\u{00FF}', "y"),
    ('\u{00C0}', "a"),
    ('\u{00C1}', "a"),
    ('\u{00C2}', "a"),
    ('\u{00C3}', "a"),
    ('\u{00C4}', "a"),
    ('\u{00C5}', "a"),
    ('\u{00C7}', "c"),
    ('\u{00C8}', "e"),
    ('\u{00C9}', "e"),
    ('\u{00CA}', "e"),
    ('\u{00CB}', "e"),
    ('\u{00CC}', "i"),
    ('\u{00CD}', "i"),
    ('\u{00CE}', "i"),
    ('\u{00CF}', "i"),
    ('\u{00D1}', "n"),
    ('\u{00D2}', "o"),
    ('\u{00D3}', "o"),
    ('\u{00D4}', "o"),
    ('\u{00D5}', "o"),
    ('\u{00D6}', "o"),
    ('\u{00D9}', "u"),
    ('\u{00DA}', "u"),
    ('\u{00DB}', "u"),
    ('\u{00DC}', "u"),
    ('\u{00DD}', "y"),
    ('\u{00DF}', "ss"),
  ]
}

///|
let default_word_map : Array[(String, String)] = []

///|
let default_drop_chars : Array[Char] = ['\'', '"']

///|
let default_separator_chars : Array[Char] = [
  ' ', '_', '-', '.', '/', '\\', '+', '&',
]

///|
struct Buckets[T](FixedArray[Array[T]])

///|
fn Buckets::new_pair(size : Int) -> Buckets[(Char, String)] {
  FixedArray::makei(size, _ => [])
}

///|
fn Buckets::new_word(size : Int) -> Buckets[(Array[Char], String)] {
  FixedArray::makei(size, _ => [])
}

///|
fn Buckets::new_char(size : Int) -> Buckets[Char] {
  FixedArray::makei(size, _ => [])
}

///|
fn bucket_index(c : Char, bucket_count : Int) -> Int {
  c.to_int() % bucket_count
}

///|
#alias("_[_]")
fn[T] Buckets::get(self : Buckets[T], c : Char) -> Array[T] {
  self.0[bucket_index(c, self.0.length())]
}

///|
fn Buckets::contains(self : Buckets[Char], c : Char) -> Bool {
  self.get(c).contains(c)
}

///|
fn Buckets::lookup_pair(self : Buckets[(Char, String)], c : Char) -> String? {
  for pair in self.get(c) {
    let (from, to) = pair
    if from == c {
      return Some(to)
    }
  }
  None
}

///|
fn normalize_ascii(c : Char, lowercase : Bool) -> Char {
  if !lowercase {
    return c
  }
  c.to_ascii_lowercase()
}

///|
fn normalize_for_slug(input : String) -> String {
  let normalized = @normalization.nfkd(input)
  let filtered : Array[Char] = []
  for c in normalized {
    if !is_combining_mark(c) {
      filtered.push(c)
    }
  }
  [..filtered]
}

///|
fn apply_word_map(input : String, rules : RuleSet) -> String {
  if rules.raw_word_map.is_empty() {
    return input
  }
  let input_chars : Array[Char] = [..input]
  let output : Array[Char] = []
  let mut index = 0
  while index < input_chars.length() {
    if find_word_match(input_chars, index, rules)
      is Some((matched_len, replacement)) {
      for c in replacement {
        output.push(c)
      }
      index = index + matched_len
    } else {
      output.push(input_chars[index])
      index = index + 1
    }
  }
  [..output]
}

///|
fn find_word_match(
  chars : Array[Char],
  index : Int,
  rules : RuleSet,
) -> (Int, String)? {
  let current = chars[index]
  let candidates = rules.word_map_buckets[current]
  let mut best_len = 0
  let mut best_replacement = ""

  for entry in candidates {
    let (key_chars, replacement) = entry
    let len = key_chars.length()
    if len == 0 {
      continue
    }
    if len <= best_len {
      continue
    }
    if !is_word_boundary(chars, index) {
      continue
    }
    if !matches_at(chars, index, key_chars) {
      continue
    }
    if !is_word_boundary(chars, index + len) {
      continue
    }
    best_len = len
    best_replacement = replacement
  }

  if best_len > 0 {
    Some((best_len, best_replacement))
  } else {
    None
  }
}

///|
fn matches_at(chars : Array[Char], index : Int, needle : Array[Char]) -> Bool {
  if index + needle.length() > chars.length() {
    return false
  }
  for i in 0.. Bool {
  if index <= 0 || index >= chars.length() {
    return true
  }
  !is_word_char(chars[index - 1]) || !is_word_char(chars[index])
}

///|
fn is_word_char(c : Char) -> Bool {
  c.is_ascii_alphabetic() || c.is_ascii_digit()
}

///|
fn is_combining_mark(c : Char) -> Bool {
  match c {
    '\u{0300}'..='\u{036F}' => true
    '\u{1AB0}'..='\u{1AFF}' => true
    '\u{1DC0}'..='\u{1DFF}' => true
    '\u{20D0}'..='\u{20FF}' => true
    '\u{FE20}'..='\u{FE2F}' => true
    _ => false
  }
}

///|
fn is_separator_candidate(c : Char, rules : RuleSet) -> Bool {
  if rules.separator_buckets.contains(c) {
    return true
  }
  match c {
    ' '..='/' => true
    ':'..='@' => true
    '['..='`' => true
    '{'..='~' => true
    _ => false
  }
}

///|
fn append_mapped(
  mapped : String,
  output : Array[Char],
  lowercase : Bool,
  separator : Char,
  rules : RuleSet,
  last_is_separator : Bool,
) -> Unit {
  let mut local_last_is_separator = last_is_separator
  for c in mapped {
    if c.is_ascii_alphabetic() || c.is_ascii_digit() {
      output.push(normalize_ascii(c, lowercase))
      local_last_is_separator = false
      continue
    }
    if is_separator_candidate(c, rules) {
      if !output.is_empty() && !local_last_is_separator {
        output.push(separator)
        local_last_is_separator = true
      }
    }
  }
}

///|
fn trim_separator_edges(chars : Array[Char], separator : Char) -> Array[Char] {
  let total = chars.length()
  let mut start = 0
  while start < total && chars[start] == separator {
    start = start + 1
  }
  let mut end = total
  while end > start && chars[end - 1] == separator {
    end = end - 1
  }
  chars[start:end].to_array()
}

///|
fn apply_max_length(chars : Array[Char], max_length : Int?) -> Array[Char] {
  if max_length is Some(limit) {
    if limit <= 0 {
      []
    } else if chars.length() <= limit {
      chars
    } else {
      chars[:limit].to_array()
    }
  } else {
    chars
  }
}