///|
fn unicode_mask(code : Int) -> Int {
  let cp = code & 65535
  let mut low = 0
  let mut high = unicode_properties.length()
  while low < high {
    let mid = (low + high) / 2
    let (first, last, mask) = unicode_properties[mid]
    if cp < first {
      high = mid
    } else if cp > last {
      low = mid + 1
    } else {
      // Tcl 8.6's category predicates use the low 16 bits of a decoded
      // codepoint. Its ASCII and hexadecimal predicates check the full value.
      return if code > 65535 { mask & 4091 } else { mask }
    }
  }
  0
}

///|
fn unicode_case_unit(code : Int, mode : Int) -> Int {
  if code < 128 {
    if mode == 0 && code >= 65 && code <= 90 {
      return code + 32
    }
    if mode != 0 && code >= 97 && code <= 122 {
      return code - 32
    }
    return code
  }
  let mut low = 0
  let mut high = unicode_cases.length()
  while low < high {
    let mid = (low + high) / 2
    let (cp, lower, upper, title) = unicode_cases[mid]
    if code < cp {
      high = mid
    } else if code > cp {
      low = mid + 1
    } else {
      return if mode == 0 { lower } else if mode == 1 { upper } else { title }
    }
  }
  code
}

///|
fn unicode_case(
  text : String,
  mode : Int,
  conversion? : Bool = false,
) -> String {
  let out = StringBuilder()
  for i in 0.. 0 { 0 } else { mode }
    let mapped = if mode == 2 && i > 0 && cp >= 7312 && cp < 7360 {
      cp
    } else {
      unicode_case_unit(cp, selected)
    }
    // Tcl's in-place UTF-8 conversion never grows a character's byte width.
    // Nocase comparisons use the unrestricted simple lowercase mapping.
    let mapped = if conversion && cp < 2048 && mapped >= 2048 {
      cp
    } else {
      mapped
    }
    out.write_char(mapped.unsafe_to_char())
  }
  out.to_string()
}

///|
fn unit_text(code : Int) -> String {
  String::from_array([code.unsafe_to_char()])
}

///|
// Tcl's exact lsearch checks encoded length before its nocase comparison.
fn tcl_utf8_length(text : String) -> Int {
  let mut length = 0
  for i in 0.. 0 && cp < 128 { 1 } else if cp < 2048 { 2 } else { 3 }
  }
  length
}

///|
fn utf16_units(text : String) -> Array[Char] {
  Array::makei(text.length(), i => {
    text.get(i).unwrap().to_int().unsafe_to_char()
  })
}

///|
// Callers have checked both offsets. Tcl permits splitting surrogate pairs;
// MoonBit's ordinary StringView slicing requires codepoint boundaries.
fn unit_slice(text : String, start : Int, end : Int) -> String {
  String::unsafe_substring(text, start~, end~)
}

///|
fn unicode_reverse(text : String) -> String {
  let out = StringBuilder()
  let mut i = text.length()
  while i > 0 {
    let last = text.get(i - 1).unwrap().to_int()
    if last >= 56320 && last <= 57343 && i >= 2 {
      let first = text.get(i - 2).unwrap().to_int()
      if first >= 55296 && first <= 56319 {
        out.write_string(unit_slice(text, i - 2, i))
        i -= 2
        continue
      }
    }
    out.write_string(unit_slice(text, i - 1, i))
    i -= 1
  }
  out.to_string()
}