///|
pub fn is_surrogate_code_unit(unit : UInt16) -> Bool {
  unit >= 0xD800 && unit <= 0xDFFF
}

///|
pub fn string_has_non_bmp(text : String) -> Bool {
  text.to_array().any(fn(c) { !c.is_bmp() })
}

///|
pub fn display_width(text : String) -> Int {
  text
  .to_array()
  .fold(init=0, (width, c) => {
    if is_cjk_char(c) || !c.is_bmp() {
      width + 2
    } else if c.is_control() {
      width
    } else {
      width + 1
    }
  })
}

///|
pub fn code_unit_offsets(text : String) -> Array[Int] {
  let result = [0]
  let mut offset = 0
  for c in text {
    offset += c.to_string().length()
    result.push(offset)
  }
  result
}

///|
pub fn char_index_to_code_unit(text : String, index : Int) -> Int {
  let offsets = code_unit_offsets(text)
  match offsets.get(index) {
    Some(value) => value
    None => offsets[offsets.length() - 1]
  }
}

///|
pub fn code_unit_to_char_index(text : String, offset : Int) -> Int {
  let offsets = code_unit_offsets(text)
  let bounded = if offset < 0 {
    0
  } else if offset > text.length() {
    text.length()
  } else {
    offset
  }
  let mut result = 0
  for i in 0.. Span {
  {
    start: char_index_to_code_unit(text, span.start),
    end: char_index_to_code_unit(text, span.end),
  }
}

///|
pub fn code_unit_span_to_chars(text : String, span : Span) -> Span {
  {
    start: code_unit_to_char_index(text, span.start),
    end: code_unit_to_char_index(text, span.end),
  }
}

///|
pub fn normalize_newlines(text : String) -> String {
  text.replace_all(old="\r\n", new="\n").replace_all(old="\r", new="\n")
}

///|
pub fn remove_zero_width_chars(text : String) -> String {
  let builder = StringBuilder()
  for c in text {
    let value = c.to_int()
    if value != 0x200B && value != 0x200C && value != 0x200D && value != 0xFEFF {
      builder.write_char(c)
    }
  }
  builder.to_string()
}

///|
pub fn sanitize_input(text : String) -> String {
  normalize_newlines(remove_zero_width_chars(text))
}

///|
pub fn safe_normalized_input(text : String) -> String {
  normalize_for_matching(sanitize_input(text))
}

///|
pub fn normalize_with_offset_notice(text : String) -> (String, Bool) {
  let normalized = sanitize_input(text)
  (normalized, normalized != text)
}

///|
pub fn replacement_boundary_is_safe(text : String, offset : Int) -> Bool {
  if offset <= 0 || offset >= text.length() {
    true
  } else {
    let left = text[offset - 1]
    let right = text[offset]
    !(left >= 0xD800 && left <= 0xDBFF && right >= 0xDC00 && right <= 0xDFFF)
  }
}

///|
pub fn safe_code_unit_boundaries(text : String) -> Array[Int] {
  let result = []
  for offset in 0..<=text.length() {
    if replacement_boundary_is_safe(text, offset) {
      result.push(offset)
    }
  }
  result
}

///|
pub fn nearest_safe_boundary(text : String, offset : Int) -> Int {
  let bounded = if offset < 0 {
    0
  } else if offset > text.length() {
    text.length()
  } else {
    offset
  }
  if replacement_boundary_is_safe(text, bounded) {
    bounded
  } else {
    let mut left = bounded
    while left > 0 && !replacement_boundary_is_safe(text, left) {
      left -= 1
    }
    left
  }
}

///|
pub fn safe_slice_by_code_units(
  text : String,
  start : Int,
  end : Int,
) -> String {
  let left = nearest_safe_boundary(text, start)
  let right = nearest_safe_boundary(text, end)
  if right < left {
    ""
  } else {
    text[left:right].to_owned()
  }
}

///|
pub fn redact_unicode_safe(
  input : String,
  config : RedactionConfig,
) -> DeidResult raise DeidError {
  let sanitized = sanitize_input(input)
  redact_with_config(sanitized, config)
}

///|
pub fn unicode_profile(text : String) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for c in text {
    let kind = if c.is_ascii() {
      "ascii"
    } else if c.is_bmp() {
      "bmp"
    } else {
      "non-bmp"
    }
    counts[kind] = counts.get_or_default(kind, 0) + 1
  }
  counts
}

///|
pub fn unicode_profile_text(text : String) -> String {
  let profile = unicode_profile(text)
  [
    "characters=\{text.char_length()}",
    "code_units=\{text.length()}",
    "display_width=\{display_width(text)}",
    "ascii=\{profile.get_or_default("ascii", 0)}",
    "bmp=\{profile.get_or_default("bmp", 0)}",
    "non_bmp=\{profile.get_or_default("non-bmp", 0)}",
  ].join("\n")
}