///|
/// Whether a line may end at a text boundary. Mandatory includes the end of
/// nonempty text, even when the text does not end in a newline character.
#derive(Eq, Debug)
pub(all) enum BreakOpportunity {
  Prohibited
  Allowed
  Mandatory
}

///|
pub extend BreakOpportunity with Eq::{equal}

///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend BreakOpportunity with Eq::{not_equal}

///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend BreakOpportunity with Debug::{to_repr}

///|
/// A forward cursor over text segments ending at default UAX #14 opportunities.
struct LineBreaks {
  text : StringView
  units : Array[BreakUnit]
  mut index : Int32
  mut offset : Int32
  mut opportunity : BreakOpportunity
}

///|
/// Find the default Unicode 18.0.0 UAX #14 line break opportunities.
///
/// Segments are contiguous views covering the input, retaining spaces and
/// newline characters. The view is processed independently of its backing
/// string. Default line breaks may fall inside extended grapheme clusters;
/// use Graphemes::line_break to query only the ends of complete clusters.
///
/// Nonempty input ends with one Mandatory boundary; empty input yields nothing.
/// Unpaired surrogates are resolved as SG -> AL; valid pairs stay together.
/// Construction prepares character context in O(n) time and space; iteration
/// scans it once without copying the text. No locale tailoring, dictionary
/// segmentation, or width-based wrapping is applied. The unicode-data
/// line_break property query remains unresolved.
///
/// ```mbt check
/// test {
///   let breaks = @segmentation.line_breaks("Hello world!")
///   inspect(breaks.next(), content="Hello ")
///   debug_inspect(breaks.opportunity(), content="Allowed")
///   inspect(breaks.next(), content="world!")
///   debug_inspect(breaks.opportunity(), content="Mandatory")
///   assert_true(breaks.next().is_empty())
/// }
/// ```
pub fn line_breaks(text : StringView) -> LineBreaks {
  {
    text,
    units: prepare_units(text),
    index: 1,
    offset: 0,
    opportunity: Prohibited,
  }
}

///|
/// Return the next segment, or an empty view at the end of the input once
/// exhausted. The returned view retains its offsets in the backing string.
/// Empty results do not change opportunity().
pub fn LineBreaks::next(self : LineBreaks) -> StringView {
  while self.index <= self.units.length() {
    let index = self.index
    self.index += 1
    let kind = if index == self.units.length() {
      Mandatory
    } else {
      boundary(self.units, index).unwrap_or(Prohibited)
    }
    if kind != Prohibited {
      let start = self.offset
      self.offset = self.units[index - 1].end
      self.opportunity = kind
      // `units` carry the UTF-16 end of every code point, so both offsets are
      // in range and on character boundaries: a raw view needs no checking.
      return self.text.unsafe_view(start~, end=self.offset)
    }
  }
  self.text.unsafe_view(start=self.offset, end=self.offset)
}

///|
/// Iterate over the remaining segments, advancing this same cursor. Query
/// opportunity() in the loop body for the segment just yielded. Collected
/// views retain their text and positions, but do not store the break kind.
pub fn LineBreaks::iter(self : LineBreaks) -> Iter[StringView] {
  Iter::new(() => {
    let segment = self.next()
    if segment.is_empty() {
      None
    } else {
      Some(segment)
    }
  })
}

///|
/// The break kind at the end of the last nonempty segment returned by next()
/// or iter(). Before the first segment, including for empty input, returns
/// Prohibited. Exhaustion preserves the last kind; querying does not advance.
pub fn LineBreaks::opportunity(self : LineBreaks) -> BreakOpportunity {
  self.opportunity
}