// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Segmentation utilities.
///
/// Upstream cosmic-text uses `unicode_segmentation` (UAX#29). This MVP provides a
/// whitespace-based word segmentation that is sufficient for ASCII/Latin text.
fn is_ws(code : Int) -> Bool {
  code == 32 || code == 9 || code == 10 || code == 13
}

///|
/// Returns word ranges `[start,end)` (UTF-16 code unit indices), splitting on whitespace.
pub fn word_indices_whitespace(s : String) -> Array[(Int, Int)] {
  let ranges : Array[(Int, Int)] = []
  let len = s.length()
  let mut i = 0
  while i < len {
    while i < len && is_ws(s.code_unit_at(i).to_int()) {
      i = i + 1
    }
    if i >= len {
      break
    }
    let start = i
    while i < len && !is_ws(s.code_unit_at(i).to_int()) {
      i = i + 1
    }
    ranges.push((start, i))
  }
  ranges
}

///|
/// Word boundary ranges based on swash Unicode analysis (UAX#29-ish).
///
/// Returns segments separated by `Boundary::Word`/`Line`/`Mandatory`.
pub fn word_indices_uax29(s : String) -> Array[(Int, Int)] {
  let chars = s.to_array()
  if chars.length() == 0 {
    return []
  }
  let analyze = @moon_swash.analyze(chars.iter())
  analyze.set_break_strength(@moon_swash.WordBreakStrength::Normal)
  let offs : Array[Int] = []
  let mut pos = 0
  for ch in chars {
    offs.push(pos)
    pos = pos + ch.utf16_len()
  }
  fn category_is_word_like(category : @moon_swash.Category) -> Bool {
    match category {
      @moon_swash.Category::CasedLetter
      | @moon_swash.Category::DecimalNumber
      | @moon_swash.Category::EnclosingMark
      | @moon_swash.Category::Letter
      | @moon_swash.Category::LetterNumber
      | @moon_swash.Category::LowercaseLetter
      | @moon_swash.Category::Mark
      | @moon_swash.Category::ModifierLetter
      | @moon_swash.Category::NonspacingMark
      | @moon_swash.Category::Number
      | @moon_swash.Category::OtherLetter
      | @moon_swash.Category::OtherNumber
      | @moon_swash.Category::SpacingMark
      | @moon_swash.Category::TitlecaseLetter
      | @moon_swash.Category::UppercaseLetter
      | @moon_swash.Category::ConnectorPunctuation => true
      _ => false
    }
  }
  fn segment_contains_word_like(
    chars : Array[Char],
    start_char_i : Int,
    end_char_i : Int,
  ) -> Bool {
    if end_char_i < start_char_i {
      return false
    }
    for i in start_char_i..<(end_char_i + 1) {
      let cat = @moon_swash.CharInfo::from_char(chars[i]).category()
      if category_is_word_like(cat) {
        return true
      }
    }
    false
  }
  let ranges : Array[(Int, Int)] = []
  let mut start_pos = 0
  let mut start_char_i = 0
  for i in 0.. break
      Some((_props, boundary)) =>
        match boundary {
          Word | Line | Mandatory => {
            // `Boundary::*` means break before current char.
            let break_pos = offs[i]
            if break_pos > start_pos &&
              segment_contains_word_like(chars, start_char_i, i - 1) {
              ranges.push((start_pos, break_pos))
            }
            start_pos = break_pos
            start_char_i = i
          }
          None => ()
        }
    }
  }
  if pos > start_pos &&
    segment_contains_word_like(chars, start_char_i, chars.length() - 1) {
    ranges.push((start_pos, pos))
  }
  ranges
}

///|
/// Word segments used for wrapping (cosmic-text-style).
///
/// Upstream uses `unicode_linebreak::linebreaks` to split into segments, then
/// splits trailing whitespace into individual blank "words".
///
/// This helper approximates that behavior using swash boundary analysis:
/// - Use `moon_swash.analyze` to identify UAX#14 linebreak opportunities:
///   split on `Boundary::Line` or `Boundary::Mandatory` (ignore `Boundary::Word`).
/// - For each segment, split trailing whitespace characters into individual blank segments
///   (mirrors upstream's `char::is_whitespace()` scan).
///
/// Returns `(start, end, blank)` ranges in UTF-16 code units.
pub fn wrap_word_segments(s : String) -> Array[(Int, Int, Bool)] {
  let chars = s.to_array()
  if chars.length() == 0 {
    return []
  }

  // Precompute UTF-16 offsets for each char.
  let offs : Array[Int] = []
  let lens : Array[Int] = []
  let mut pos = 0
  for ch in chars {
    offs.push(pos)
    let len = ch.utf16_len()
    lens.push(len)
    pos = pos + len
  }
  fn emit_segment(
    out : Array[(Int, Int, Bool)],
    chars : Array[Char],
    offs : Array[Int],
    lens : Array[Int],
    start_char_i : Int,
    end_char_i : Int,
    start_word : Int,
    end_pos : Int,
  ) -> Unit {
    if end_pos <= start_word || end_char_i < start_char_i {
      return
    }
    // Find the start of trailing whitespace inside [start_word, end_pos).
    let mut start_lb = end_pos
    let mut ws_start_i : Int? = None
    let mut k = end_char_i
    while k >= start_char_i {
      if chars[k].is_whitespace() {
        start_lb = offs[k]
        ws_start_i = Some(k)
      } else {
        break
      }
      if k == 0 {
        break
      }
      k = k - 1
    }

    // Emit non-whitespace word segment.
    if start_word < start_lb {
      out.push((start_word, start_lb, false))
    }

    // Emit trailing whitespace as individual blank segments.
    match ws_start_i {
      None => ()
      Some(i0) =>
        for i in i0..<(end_char_i + 1) {
          out.push((offs[i], offs[i] + lens[i], true))
        }
    }
  }

  // Collect linebreak boundaries from the analyzer.
  let analyze = @moon_swash.analyze(chars.iter())
  analyze.set_break_strength(@moon_swash.WordBreakStrength::Normal)
  let out : Array[(Int, Int, Bool)] = []
  let mut start_char_i = 0
  let mut start_word = 0

  // `Analyze` reports `Boundary::Line|Mandatory` at the character that begins a new segment
  // (i.e. the break is *before* the current char). This matches UAX#14 `linebreaks()` behavior
  // when interpreted as end positions.
  for i in 0.. break
      Some((_props, boundary)) =>
        match boundary {
          Line | Mandatory => {
            let end_pos = offs[i]
            emit_segment(
              out,
              chars,
              offs,
              lens,
              start_char_i,
              i - 1,
              start_word,
              end_pos,
            )
            start_char_i = i
            start_word = end_pos
          }
          _ => ()
        }
    }
  }

  // Final segment (end-of-string).
  emit_segment(
    out,
    chars,
    offs,
    lens,
    start_char_i,
    chars.length() - 1,
    start_word,
    s.length(),
  )
  out
}

///|
/// Grapheme cluster ranges `[start,end)` in UTF-16 code units (UAX#29-ish).
///
/// This is sufficient for making Backspace/Delete operate on user-perceived characters.
pub fn grapheme_indices_uax29(s : String) -> Array[(Int, Int)] {
  let chars = s.to_array()
  if chars.length() == 0 {
    return []
  }
  let offs : Array[Int] = []
  let lens : Array[Int] = []
  let breaks : Array[@moon_swash.ClusterBreak] = []
  let pict : Array[Bool] = []
  let mut pos = 0
  for ch in chars {
    offs.push(pos)
    let len = ch.utf16_len()
    lens.push(len)
    let info = @moon_swash.CharInfo::from_char(ch)
    breaks.push(info.cluster_break())
    pict.push(info.properties().is_extended_pictographic())
    pos = pos + len
  }
  fn is_control(b : @moon_swash.ClusterBreak) -> Bool {
    match b {
      CN | CR | LF => true
      _ => false
    }
  }

  fn is_hangul_l(b : @moon_swash.ClusterBreak) -> Bool {
    match b {
      L => true
      _ => false
    }
  }

  fn is_hangul_v(b : @moon_swash.ClusterBreak) -> Bool {
    match b {
      V | LV => true
      _ => false
    }
  }

  fn is_hangul_t(b : @moon_swash.ClusterBreak) -> Bool {
    match b {
      T | LVT => true
      _ => false
    }
  }

  fn should_break(
    i : Int,
    breaks : Array[@moon_swash.ClusterBreak],
    pict : Array[Bool],
  ) -> Bool {
    let prev = breaks[i - 1]
    let cur = breaks[i]

    // GB3
    if prev is CR && cur is LF {
      return false
    }
    // GB4/GB5
    if is_control(prev) || is_control(cur) {
      return true
    }
    // GB6
    if is_hangul_l(prev) {
      match cur {
        L | V | LV | LVT => return false
        _ => ()
      }
    }
    // GB7
    if is_hangul_v(prev) {
      match cur {
        V | T => return false
        _ => ()
      }
    }
    // GB8
    if is_hangul_t(prev) {
      if cur is T {
        return false
      }
    }
    // GB9/9a/9b
    match cur {
      EX | ZWJ | SM => return false
      _ => ()
    }
    if prev is PP {
      return false
    }
    // GB11 (simplified): Extended_Pictographic Extend* ZWJ x Extended_Pictographic
    if cur is @moon_swash.ClusterBreak::XX && pict[i] && prev is ZWJ {
      let mut k = i - 2
      while k >= 0 && breaks[k] is EX {
        k = k - 1
      }
      if k >= 0 && pict[k] {
        return false
      }
    }
    // GB12/13: RI pairs
    if prev is RI && cur is RI {
      let mut count = 0
      let mut k = i - 1
      while k >= 0 && breaks[k] is RI {
        count = count + 1
        if k == 0 {
          break
        }
        k = k - 1
      }
      if count % 2 == 1 {
        return false
      }
    }
    true
  }

  let ranges : Array[(Int, Int)] = []
  let mut start = 0
  for i in 1..