///|
/// Shared text segmentation and codepoint classification. Skia and Sun used
/// to each maintain their own grapheme-cluster/bidi-visual-order/mixed-segment
/// pipeline and identical codepoint classifiers for fallback diagnostics.
/// This module owns that pure logic once; renderers keep their native text
/// engines and call back into these helpers.

///|
/// Split text into grapheme clusters via the shared `@core` UAX-style
/// boundary scanner, then order runs visually (RTL runs reversed).
pub fn text_bidi_visual_clusters(text : String) -> Array[String] {
  let clusters = text_grapheme_clusters(text)
  let visual : Array[String] = []
  let mut index = 0
  while index < clusters.length() {
    let rtl = text_cluster_is_rtl(clusters[index])
    let start = index
    index = index + 1
    while index < clusters.length() &&
          text_cluster_is_rtl(clusters[index]) == rtl {
      index = index + 1
    }
    if rtl {
      let mut cursor = index
      while cursor > start {
        cursor = cursor - 1
        visual.push(clusters[cursor])
      }
    } else {
      for cursor in start.. Array[String] {
  let clusters = text_bidi_visual_clusters(text)
  let segments : Array[String] = []
  let current : Array[Char] = []
  let mut current_class = -1
  for cluster in clusters {
    let cluster_class = text_segment_class(cluster)
    if !current.is_empty() && cluster_class != current_class {
      segments.push(String::from_array(current))
      current.clear()
    }
    for ch in cluster {
      current.push(ch)
    }
    current_class = cluster_class
  }
  if !current.is_empty() {
    segments.push(String::from_array(current))
  }
  segments
}

///|
fn text_cluster_is_rtl(cluster : String) -> Bool {
  text_segment_class(cluster) == 4
}

///|
/// Join grapheme clusters back into a single string.
pub fn text_join_clusters(clusters : Array[String]) -> String {
  let chars : Array[Char] = []
  for cluster in clusters {
    for ch in cluster {
      chars.push(ch)
    }
  }
  String::from_array(chars)
}

///|
pub fn text_string_arrays_equal(
  left : Array[String],
  right : Array[String],
) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for index in 0.. Array[String] {
  let chars = text.to_array()
  let boundaries = @core.TextGraphemeBoundaries::new(text~).boundaries()
  let clusters : Array[String] = []
  if boundaries.length() < 2 {
    return clusters
  }
  for boundary_index in 0..<(boundaries.length() - 1) {
    let start = boundaries[boundary_index]
    let end = boundaries[boundary_index + 1]
    if end > start {
      clusters.push(String::from_array(chars[start:end]))
    }
  }
  clusters
}

///|
/// Classify a text segment for fallback planning: 1 emoji, 2 CJK, 3 Hangul,
/// 4 RTL script, 5 combining marks, 0 other.
pub fn text_segment_class(text : String) -> Int {
  let representative = representative_fallback_codepoint(
    text,
    skip_joiners=false,
  )
  if codepoint_is_emoji_hint(representative) {
    1
  } else if codepoint_is_cjk(representative) {
    2
  } else if codepoint_is_hangul_cluster_codepoint(representative) {
    3
  } else if codepoint_is_rtl_script(representative) {
    4
  } else if representative < 0x0300 {
    0
  } else {
    5
  }
}

///|
pub fn codepoint_is_cjk(codepoint : Int) -> Bool {
  codepoint_is_cjk_ideograph(codepoint) || codepoint_is_kana(codepoint)
}

///|
pub fn codepoint_is_cjk_ideograph(codepoint : Int) -> Bool {
  (codepoint >= 0x3400 && codepoint <= 0x4DBF) ||
  (codepoint >= 0x4E00 && codepoint <= 0x9FFF) ||
  (codepoint >= 0xF900 && codepoint <= 0xFAFF) ||
  (codepoint >= 0x20000 && codepoint <= 0x2FA1F)
}

///|
pub fn codepoint_is_kana(codepoint : Int) -> Bool {
  codepoint >= 0x3040 && codepoint <= 0x30FF
}

///|
pub fn codepoint_is_rtl_script(codepoint : Int) -> Bool {
  (codepoint >= 0x0590 && codepoint <= 0x08FF) ||
  (codepoint >= 0xFB1D && codepoint <= 0xFDFF) ||
  (codepoint >= 0xFE70 && codepoint <= 0xFEFF)
}

///|
pub fn codepoint_is_emoji_hint(codepoint : Int) -> Bool {
  codepoint == 0x200D ||
  codepoint == 0xFE0F ||
  (codepoint >= 0x2600 && codepoint <= 0x27BF) ||
  (codepoint >= 0x1F000 && codepoint <= 0x1FAFF)
}

///|
pub fn codepoint_is_hangul_cluster_codepoint(codepoint : Int) -> Bool {
  codepoint_is_hangul_l(codepoint) ||
  codepoint_is_hangul_v(codepoint) ||
  codepoint_is_hangul_t(codepoint) ||
  codepoint_is_hangul_lv(codepoint) ||
  codepoint_is_hangul_lvt(codepoint)
}

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

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

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

///|
pub fn codepoint_is_hangul_lv(codepoint : Int) -> Bool {
  codepoint_is_hangul_syllable(codepoint) && (codepoint - 0xAC00) % 28 == 0
}

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

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

///|
pub fn codepoint_is_hangul_compatibility_jamo(codepoint : Int) -> Bool {
  codepoint >= 0x3130 && codepoint <= 0x318F
}

///|
/// Representative codepoint for fallback planning: the first emoji-hint
/// codepoint (optionally skipping ZWJ/VS16 joiners), else the first
/// non-ASCII codepoint, else the first codepoint.
pub fn representative_fallback_codepoint(
  text : String,
  skip_joiners? : Bool = false,
) -> Int {
  let mut first = 0
  let mut first_non_ascii = 0
  for ch in text {
    let codepoint = ch.to_int()
    if first == 0 {
      first = codepoint
    }
    if first_non_ascii == 0 && codepoint >= 0x80 {
      first_non_ascii = codepoint
    }
    if codepoint_is_emoji_hint(codepoint) &&
      (!skip_joiners || (codepoint != 0x200D && codepoint != 0xFE0F)) {
      return codepoint
    }
  }
  if first_non_ascii > 0 {
    first_non_ascii
  } else {
    first
  }
}