///| A lightweight half-open codepoint range `[start, end)` used by the

///| grapheme boundary API. It mirrors `@core.TextRange` but keeps this

///|
/// package free of any core dependency.
pub(all) struct GraphemeRange {
  start : Int
  end : Int
} derive(Eq, Debug)

///|
pub fn GraphemeRange::new(start~ : Int, end~ : Int) -> GraphemeRange {
  { start, end }
}

///|
pub fn GraphemeRange::collapsed(index : Int) -> GraphemeRange {
  { start: index, end: index }
}

///|
pub fn GraphemeRange::normalized(self : GraphemeRange) -> GraphemeRange {
  if self.start <= self.end {
    self
  } else {
    { start: self.end, end: self.start }
  }
}

///|
pub fn GraphemeRange::is_collapsed(self : GraphemeRange) -> Bool {
  self.start == self.end
}

///|
fn max_int(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn clamp_int(value : Int, min : Int, max : Int) -> Int {
  if value < min {
    min
  } else if value > max {
    max
  } else {
    value
  }
}

///|
pub struct TextGraphemeBoundaries {
  priv text : String
  priv boundaries : Array[Int]
} derive(Eq, Debug)

///|
pub fn TextGraphemeBoundaries::new(text~ : String) -> TextGraphemeBoundaries {
  let chars = text.to_array()
  let boundaries : Array[Int] = [0]
  let mut index = 1
  let mut last_boundary = 0
  while index < chars.length() {
    if fallback_grapheme_should_break(chars, last_boundary, index) {
      boundaries.push(index)
      last_boundary = index
    }
    index = index + 1
  }
  if chars.length() > 0 {
    boundaries.push(chars.length())
  }
  { text, boundaries }
}

///|
pub fn TextGraphemeBoundaries::text(self : TextGraphemeBoundaries) -> String {
  self.text
}

///|
pub fn TextGraphemeBoundaries::boundaries(
  self : TextGraphemeBoundaries,
) -> Array[Int] {
  self.boundaries.copy()
}

///|
pub fn TextGraphemeBoundaries::length(self : TextGraphemeBoundaries) -> Int {
  match self.boundaries.last() {
    Some(length) => length
    None => 0
  }
}

///|
pub fn TextGraphemeBoundaries::cluster_count(
  self : TextGraphemeBoundaries,
) -> Int {
  max_int(0, self.boundaries.length() - 1)
}

///|
pub fn TextGraphemeBoundaries::is_boundary(
  self : TextGraphemeBoundaries,
  index : Int,
) -> Bool {
  let index = self.clamp_index(index)
  self.boundaries.any(boundary => boundary == index)
}

///|
pub fn TextGraphemeBoundaries::clamp_index(
  self : TextGraphemeBoundaries,
  index : Int,
) -> Int {
  clamp_int(index, 0, self.length())
}

///|
pub fn TextGraphemeBoundaries::previous_boundary(
  self : TextGraphemeBoundaries,
  caret : Int,
) -> Int {
  let caret = self.clamp_index(caret)
  if caret <= 0 {
    return 0
  }
  let mut previous = 0
  for boundary in self.boundaries {
    if boundary >= caret {
      return previous
    }
    previous = boundary
  }
  previous
}

///|
pub fn TextGraphemeBoundaries::next_boundary(
  self : TextGraphemeBoundaries,
  caret : Int,
) -> Int {
  let caret = self.clamp_index(caret)
  let length = self.length()
  if caret >= length {
    return length
  }
  for boundary in self.boundaries {
    if boundary > caret {
      return boundary
    }
  }
  length
}

///|
pub fn TextGraphemeBoundaries::floor_boundary(
  self : TextGraphemeBoundaries,
  index : Int,
) -> Int {
  let index = self.clamp_index(index)
  let mut floor = 0
  for boundary in self.boundaries {
    if boundary > index {
      return floor
    }
    floor = boundary
  }
  floor
}

///|
pub fn TextGraphemeBoundaries::ceil_boundary(
  self : TextGraphemeBoundaries,
  index : Int,
) -> Int {
  let index = self.clamp_index(index)
  for boundary in self.boundaries {
    if boundary >= index {
      return boundary
    }
  }
  self.length()
}

///|
pub fn TextGraphemeBoundaries::nearest_boundary(
  self : TextGraphemeBoundaries,
  index : Int,
) -> Int {
  let index = self.clamp_index(index)
  let floor = self.floor_boundary(index)
  let ceil = self.ceil_boundary(index)
  if index - floor <= ceil - index {
    floor
  } else {
    ceil
  }
}

///|
pub fn TextGraphemeBoundaries::normalize_range(
  self : TextGraphemeBoundaries,
  range : GraphemeRange,
) -> GraphemeRange {
  let normalized = range.normalized()
  if normalized.is_collapsed() {
    GraphemeRange::collapsed(self.nearest_boundary(normalized.start))
  } else {
    GraphemeRange::new(
      start=self.floor_boundary(normalized.start),
      end=self.ceil_boundary(normalized.end),
    )
  }
}

///|
pub fn TextGraphemeBoundaries::delete_range(
  self : TextGraphemeBoundaries,
  caret : Int,
  before : Int,
  after : Int,
) -> GraphemeRange {
  let mut start = self.nearest_boundary(caret)
  let mut end = start
  for _ in 0.. Int {
  let chars = self.text.to_array()
  let index = self.clamp_index(index)
  @utf8.encode(String::from_array(chars[:index])[:], bom=false).length()
}

///|
pub fn TextGraphemeBoundaries::nearest_boundary_utf8_offset(
  self : TextGraphemeBoundaries,
  index : Int,
) -> Int {
  self.utf8_offset(self.nearest_boundary(index))
}

///|
pub fn stabilize_grapheme_cluster_carets(
  text : String,
  carets : Array[Double],
) -> Array[Double] {
  let chars = text.to_array()
  if carets.length() != chars.length() + 1 {
    return carets
  }
  let adjusted = carets.copy()
  let boundaries = TextGraphemeBoundaries::new(text~).boundaries()
  for boundary_index in 0..<(boundaries.length() - 1) {
    let start = boundaries[boundary_index]
    let end = boundaries[boundary_index + 1]
    if end > start + 1 {
      stabilize_caret_cluster(adjusted, start, end)
    }
  }
  adjusted
}

///|
fn fallback_grapheme_should_break(
  chars : Array[Char],
  last_boundary : Int,
  index : Int,
) -> Bool {
  if index <= 0 || index >= chars.length() {
    return true
  }
  let left = chars[index - 1].to_int()
  let right = chars[index].to_int()
  if fallback_codepoint_is_cr(left) && fallback_codepoint_is_lf(right) {
    return false
  }
  if fallback_codepoint_is_grapheme_break_control(left) ||
    fallback_codepoint_is_grapheme_break_control(right) {
    return true
  }
  if fallback_hangul_allows_no_break(left, right) {
    return false
  }
  if fallback_codepoint_is_grapheme_extend(right) ||
    fallback_codepoint_is_zwj(right) {
    return false
  }
  if fallback_codepoint_is_spacing_mark(right) {
    return false
  }
  if fallback_codepoint_is_grapheme_prepend(left) {
    return false
  }
  if fallback_indic_conjunct_allows_no_break(chars, index) {
    return false
  }
  if fallback_emoji_zwj_allows_no_break(chars, index) {
    return false
  }
  if fallback_codepoint_is_regional_indicator(left) &&
    fallback_codepoint_is_regional_indicator(right) {
    let ri_count = fallback_regional_indicator_count_before(
      chars, last_boundary, index,
    )
    return ri_count % 2 == 0
  }
  true
}

///|
fn stabilize_caret_cluster(
  carets : Array[Double],
  start : Int,
  end : Int,
) -> Unit {
  if start < 0 || end <= start + 1 || end >= carets.length() {
    return ()
  }
  let cluster_start = carets[start]
  for caret_index in (start + 1).. Int {
  TextGraphemeBoundaries::new(text~).previous_boundary(caret)
}

///|
pub fn next_grapheme_caret_boundary(text : String, caret : Int) -> Int {
  TextGraphemeBoundaries::new(text~).next_boundary(caret)
}

///|
fn fallback_codepoint_is_cr(codepoint : Int) -> Bool {
  unicode_grapheme_is_cr_17_0(codepoint)
}

///|
fn fallback_codepoint_is_lf(codepoint : Int) -> Bool {
  unicode_grapheme_is_lf_17_0(codepoint)
}

///|
fn fallback_codepoint_is_grapheme_break_control(codepoint : Int) -> Bool {
  fallback_codepoint_is_cr(codepoint) ||
  fallback_codepoint_is_lf(codepoint) ||
  unicode_grapheme_is_control_17_0(codepoint)
}

///|
fn fallback_codepoint_is_zwj(codepoint : Int) -> Bool {
  unicode_grapheme_is_zwj_17_0(codepoint)
}

///|
fn fallback_hangul_allows_no_break(left : Int, right : Int) -> Bool {
  if fallback_codepoint_is_hangul_l(left) {
    return fallback_codepoint_is_hangul_l(right) ||
      fallback_codepoint_is_hangul_v(right) ||
      fallback_codepoint_is_hangul_lv(right) ||
      fallback_codepoint_is_hangul_lvt(right)
  }
  if fallback_codepoint_is_hangul_lv(left) ||
    fallback_codepoint_is_hangul_v(left) {
    return fallback_codepoint_is_hangul_v(right) ||
      fallback_codepoint_is_hangul_t(right)
  }
  if fallback_codepoint_is_hangul_lvt(left) ||
    fallback_codepoint_is_hangul_t(left) {
    return fallback_codepoint_is_hangul_t(right)
  }
  false
}

///|
fn fallback_indic_conjunct_allows_no_break(
  chars : Array[Char],
  index : Int,
) -> Bool {
  let right = chars[index].to_int()
  if !fallback_codepoint_is_indic_conjunct_consonant(right) {
    return false
  }
  let mut cursor = index - 1
  let mut saw_linker = false
  while cursor >= 0 {
    let codepoint = chars[cursor].to_int()
    if fallback_codepoint_is_indic_conjunct_linker(codepoint) {
      saw_linker = true
      if cursor == 0 {
        return false
      }
      cursor = cursor - 1
      continue
    }
    if fallback_codepoint_is_indic_conjunct_extend(codepoint) {
      if cursor == 0 {
        return false
      }
      cursor = cursor - 1
      continue
    }
    return saw_linker &&
      fallback_codepoint_is_indic_conjunct_consonant(codepoint)
  }
  false
}

///|
fn fallback_codepoint_is_indic_conjunct_linker(codepoint : Int) -> Bool {
  unicode_indic_conjunct_is_linker_17_0(codepoint)
}

///|
fn fallback_codepoint_is_indic_conjunct_consonant(codepoint : Int) -> Bool {
  unicode_indic_conjunct_is_consonant_17_0(codepoint)
}

///|
fn fallback_codepoint_is_indic_conjunct_extend(codepoint : Int) -> Bool {
  unicode_indic_conjunct_is_extend_17_0(codepoint)
}

///|
fn fallback_emoji_zwj_allows_no_break(chars : Array[Char], index : Int) -> Bool {
  if index <= 0 || !fallback_codepoint_is_zwj(chars[index - 1].to_int()) {
    return false
  }
  if !fallback_codepoint_is_extended_pictographic(chars[index].to_int()) {
    return false
  }
  let mut cursor = index - 2
  while cursor >= 0 &&
        fallback_codepoint_is_grapheme_extend(chars[cursor].to_int()) &&
        !fallback_codepoint_is_zwj(chars[cursor].to_int()) {
    if cursor == 0 {
      return fallback_codepoint_is_extended_pictographic(chars[cursor].to_int())
    }
    cursor = cursor - 1
  }
  cursor >= 0 &&
  fallback_codepoint_is_extended_pictographic(chars[cursor].to_int())
}

///|
fn fallback_regional_indicator_count_before(
  chars : Array[Char],
  last_boundary : Int,
  index : Int,
) -> Int {
  let mut count = 0
  let mut cursor = index - 1
  while cursor >= last_boundary &&
        fallback_codepoint_is_regional_indicator(chars[cursor].to_int()) {
    count = count + 1
    if cursor == 0 {
      return count
    }
    cursor = cursor - 1
  }
  count
}

///|
fn fallback_codepoint_is_grapheme_extend(codepoint : Int) -> Bool {
  unicode_grapheme_is_extend_17_0(codepoint)
}

///|
fn fallback_codepoint_is_grapheme_prepend(codepoint : Int) -> Bool {
  unicode_grapheme_is_prepend_17_0(codepoint)
}

///|
fn fallback_codepoint_is_spacing_mark(codepoint : Int) -> Bool {
  unicode_grapheme_is_spacing_mark_17_0(codepoint)
}

///|
fn fallback_codepoint_is_hangul_l(codepoint : Int) -> Bool {
  (codepoint >= 0x1100 && codepoint <= 0x115F) ||
  (codepoint >= 0xA960 && codepoint <= 0xA97C)
}

///|
fn fallback_codepoint_is_hangul_v(codepoint : Int) -> Bool {
  (codepoint >= 0x1160 && codepoint <= 0x11A7) ||
  (codepoint >= 0xD7B0 && codepoint <= 0xD7C6)
}

///|
fn fallback_codepoint_is_hangul_t(codepoint : Int) -> Bool {
  (codepoint >= 0x11A8 && codepoint <= 0x11FF) ||
  (codepoint >= 0xD7CB && codepoint <= 0xD7FB)
}

///|
fn fallback_codepoint_is_hangul_lv(codepoint : Int) -> Bool {
  fallback_codepoint_is_hangul_syllable(codepoint) &&
  (codepoint - 0xAC00) % 28 == 0
}

///|
fn fallback_codepoint_is_hangul_lvt(codepoint : Int) -> Bool {
  fallback_codepoint_is_hangul_syllable(codepoint) &&
  (codepoint - 0xAC00) % 28 != 0
}

///|
fn fallback_codepoint_is_hangul_syllable(codepoint : Int) -> Bool {
  codepoint >= 0xAC00 && codepoint <= 0xD7A3
}

///|
fn fallback_codepoint_is_extended_pictographic(codepoint : Int) -> Bool {
  unicode_emoji_is_extended_pictographic_17_0(codepoint)
}

///|
fn fallback_codepoint_is_regional_indicator(codepoint : Int) -> Bool {
  unicode_grapheme_is_regional_indicator_17_0(codepoint)
}