///|
fn paragraph(c : @data.SentenceBreak) -> Bool {
  match c {
    Sep | CR | LF => true
    _ => false
  }
}

///|
fn terminal(c : @data.SentenceBreak) -> Bool {
  match c {
    STerm | ATerm => true
    _ => false
  }
}

///|
fn sentence_break_at(cs : Array[@data.SentenceBreak], i : Int) -> Bool {
  let a = cs[i - 1]
  let b = cs[i]
  if a == CR && b == LF {
    return false
  } // SB3
  if paragraph(a) {
    return true
  } // SB4
  // SB5 was applied during tokenization.
  if a == ATerm && b == Numeric {
    return false
  } // SB6
  if a == ATerm &&
    b == Upper &&
    i >= 2 &&
    (cs[i - 2] == Upper || cs[i - 2] == Lower) {
    return false
  } // SB7
  let mut p = i - 1
  while p >= 0 && cs[p] == Sp {
    p -= 1
  }
  while p >= 0 && cs[p] == Close {
    p -= 1
  }
  let after_terminal = p >= 0 && terminal(cs[p])
  if p >= 0 && cs[p] == ATerm { // SB8
    let mut q = i
    while q < cs.length() {
      let c = cs[q]
      if c == Lower {
        return false
      }
      if c == OLetter || c == Upper || paragraph(c) || terminal(c) {
        break
      }
      q += 1
    }
  }
  if after_terminal && (b == SContinue || terminal(b)) {
    return false
  } // SB8a
  let mut close = i - 1
  while close >= 0 && cs[close] == Close {
    close -= 1
  }
  if close >= 0 &&
    terminal(cs[close]) &&
    (b == Close || b == Sp || paragraph(b)) {
    return false
  } // SB9
  if after_terminal && (b == Sp || paragraph(b)) {
    return false
  } // SB10
  after_terminal // SB11, otherwise SB998
}

///|
/// Unicode 17 UAX #29 sentence boundaries as UTF-8 byte offsets. Includes both
/// endpoints. Separators and trailing spaces belong to the preceding sentence.
pub fn sentence_boundaries(s : String) -> Array[Int] {
  let text = @text.scan(s)
  let classes : Array[@data.SentenceBreak] = []
  let positions : Array[Int] = []
  for i, cp in text.chars {
    let c = @data.sentence_break(cp)
    if c == Extend || c == Format {
      if !classes.is_empty() && !paragraph(classes[classes.length() - 1]) {
        continue
      }
      classes.push(Other)
    } else {
      classes.push(c)
    }
    positions.push(text.bytes[i])
  }
  let result = [0]
  for i = 1; i < classes.length(); i = i + 1 {
    if sentence_break_at(classes, i) {
      result.push(positions[i])
    }
  }
  if !text.chars.is_empty() {
    result.push(text.bytes[text.chars.length()])
  }
  result
}

///|
/// Split sentences without dropping punctuation, whitespace or paragraph breaks.
pub fn sentences(s : String) -> Array[String] {
  boundary_strings(s, sentence_boundaries(s))
}

///|
/// Select the default Unicode boundary algorithm.
pub(all) enum SegmentKind {
  Word
  Sentence
} derive(Eq, Debug)

///|
pub extend SegmentKind with Eq::{not_equal, equal}

///|
pub extend SegmentKind with Debug::{to_repr}

///|
/// All half-open UTF-8 byte ranges, including whitespace/punctuation segments.
pub fn segments(s : String, kind : SegmentKind) -> Array[(Int, Int)] {
  let boundaries = match kind {
    Word => word_boundaries(s)
    Sentence => sentence_boundaries(s)
  }
  let result = []
  for i = 1; i < boundaries.length(); i = i + 1 {
    result.push((boundaries[i - 1], boundaries[i]))
  }
  result
}