///|
priv struct SunTextLine {
  text : String
  width : Double
  start_scalar : Int
  scalar_count : Int
}

///|
priv struct SunTextLinePlan {
  lines : Array[SunTextLine]
  line_height : Double
  baseline : Double
}

///|
priv enum SunTextLayoutToken {
  Text(String, Int)
  LineBreak(Int)
}

///|
fn sun_text_line_plan(
  faces : Array[@sun_text.FontFace],
  text : String,
  config : @sun_text.LayoutConfig,
) -> SunTextLinePlan {
  let lines : Array[SunTextLine] = []
  let line_height = config.line_height.max(1.0)
  let baseline = sun_text_baseline_for_faces(faces, config)
  let tokens = sun_text_layout_tokens(text)
  let mut current_text = ""
  let mut current_width = 0.0
  let mut current_start = 0
  let mut current_scalar_count = 0
  let mut cursor_scalar = 0
  let mut saw_line = false
  for token in tokens {
    match token {
      Text(token_text, token_scalar_count) => {
        let token_width = sun_measure_fallback_line_width(
          faces, token_text, config,
        )
        let is_space = token_text is " "
        let needs_break = current_scalar_count > 0 &&
          !is_space &&
          config.max_width > 0.0 &&
          current_width + token_width > config.max_width
        if needs_break {
          sun_text_line_plan_push(
            lines, faces, config, current_text, current_start, current_scalar_count,
          )
          saw_line = true
          current_start = cursor_scalar
          current_text = ""
          current_width = 0.0
          current_scalar_count = 0
        }
        current_text = current_text + token_text
        current_width = current_width + token_width
        current_scalar_count += token_scalar_count
        cursor_scalar += token_scalar_count
      }
      LineBreak(line_break_scalar_count) => {
        sun_text_line_plan_push(
          lines, faces, config, current_text, current_start, current_scalar_count,
        )
        saw_line = true
        cursor_scalar += line_break_scalar_count
        current_start = cursor_scalar
        current_text = ""
        current_width = 0.0
        current_scalar_count = 0
      }
    }
  }
  if current_scalar_count > 0 || saw_line || lines.is_empty() {
    sun_text_line_plan_push(
      lines, faces, config, current_text, current_start, current_scalar_count,
    )
  }
  { lines, line_height, baseline, }
}

///|
fn sun_text_line_plan_push(
  lines : Array[SunTextLine],
  faces : Array[@sun_text.FontFace],
  config : @sun_text.LayoutConfig,
  text : String,
  start_scalar : Int,
  scalar_count : Int,
) -> Unit {
  lines.push({
    text,
    width: sun_measure_fallback_line_width(faces, text, config),
    start_scalar,
    scalar_count,
  })
}

///|
fn sun_text_layout_tokens(text : String) -> Array[SunTextLayoutToken] {
  let tokens : Array[SunTextLayoutToken] = []
  let chars = text.to_array()
  let current_word : Array[Char] = []
  let mut current_word_class : @sun_text.LineBreakClass? = None
  let mut index = 0
  while index < chars.length() {
    let ch = chars[index]
    if ch is '\n' || ch is '\r' {
      sun_text_layout_tokens_flush_word(tokens, current_word)
      let line_break_scalar_count = if ch is '\r' &&
        index + 1 < chars.length() &&
        chars[index + 1] is '\n' {
        index += 1
        2
      } else {
        1
      }
      tokens.push(LineBreak(line_break_scalar_count))
      current_word_class = None
    } else if ch is ' ' || ch is '\t' {
      sun_text_layout_tokens_flush_word(tokens, current_word)
      current_word_class = None
      if ch is ' ' {
        tokens.push(Text(" ", 1))
      }
    } else {
      let curr_class = @sun_text.get_line_break_class(ch.to_int())
      match current_word_class {
        Some(prev_class) =>
          if sun_should_split_unspaced_layout_token(prev_class, curr_class) {
            sun_text_layout_tokens_flush_word(tokens, current_word)
            current_word_class = None
          }
        None => ()
      }
      current_word.push(ch)
      current_word_class = Some(curr_class)
    }
    index += 1
  }
  sun_text_layout_tokens_flush_word(tokens, current_word)
  tokens
}

///|
fn sun_text_layout_tokens_flush_word(
  tokens : Array[SunTextLayoutToken],
  current_word : Array[Char],
) -> Unit {
  if current_word.length() > 0 {
    tokens.push(Text(String::from_array(current_word), current_word.length()))
    current_word.clear()
  }
}

///|
fn sun_should_split_unspaced_layout_token(
  prev_class : @sun_text.LineBreakClass,
  curr_class : @sun_text.LineBreakClass,
) -> Bool {
  @sun_text.is_line_break_allowed(prev_class, curr_class) &&
  sun_can_start_unspaced_layout_token(curr_class) &&
  (
    sun_is_unspaced_layout_break_class(prev_class) ||
    sun_is_unspaced_layout_break_class(curr_class)
  )
}

///|
fn sun_can_start_unspaced_layout_token(
  class : @sun_text.LineBreakClass,
) -> Bool {
  match class {
    CL => false
    CP => false
    EX => false
    IS => false
    SY => false
    NS => false
    _ => true
  }
}

///|
fn sun_is_unspaced_layout_break_class(class : @sun_text.LineBreakClass) -> Bool {
  match class {
    ID => true
    H2 => true
    H3 => true
    JL => true
    JV => true
    JT => true
    _ => false
  }
}

///|
fn SunRasterRenderer::paragraph_layout_result(
  self : SunRasterRenderer,
  input : @core.TextLayoutInput,
) -> @core.TextParagraphLayoutResult {
  let faces = self.resolved_faces(input.font)
  if faces.is_empty() {
    return @core.TextSystem::new(id="sun-raster-text-system", measure=measure_input => {
      self.measure_text_layout(measure_input)
    }).layout_paragraph(input)
  }
  let config = sun_text_layout_config(
    input.font,
    input.max_width.unwrap_or(1000000000.0),
  )
  let plan = sun_text_line_plan(faces, input.text, config)
  let text_length = input.text.to_array().length()
  let caret_rects : Array[@core.Rect] = []
  for _ in 0..<(text_length + 1) {
    caret_rects.push(
      @core.Rect::new(x=0.0, y=0.0, width=1.0, height=plan.line_height),
    )
  }
  let lines : Array[@core.TextParagraphLineMetrics] = []
  let mut y = 0.0
  let mut width = 0.0
  let mut first_baseline = 0.0
  for line_index in 0.. @core.TextParagraphVisualOrderMetadata {
  let logical_clusters = sun_text_grapheme_clusters(text)
  let visual_clusters = sun_text_bidi_visual_clusters(logical_clusters)
  {
    logical_text: sun_text_join_clusters(logical_clusters),
    visual_text: sun_text_join_clusters(visual_clusters),
    logical_cluster_count: logical_clusters.length(),
    visual_cluster_count: visual_clusters.length(),
    segment_count: sun_text_segment_count(visual_clusters),
    glyph_count: text.to_array().length(),
    visual_order_differs: !sun_text_cluster_arrays_equal(
      logical_clusters, visual_clusters,
    ),
    bidi_visual_order_ready: true,
  }
}

///|
fn sun_text_grapheme_clusters(text : String) -> 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
}

///|
fn sun_text_bidi_visual_clusters(clusters : Array[String]) -> Array[String] {
  let visual : Array[String] = []
  let mut index = 0
  while index < clusters.length() {
    let rtl = sun_text_cluster_is_rtl(clusters[index])
    let start = index
    index = index + 1
    while index < clusters.length() &&
          sun_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.. Bool {
  let representative = @render_common.representative_fallback_codepoint(
    cluster,
    skip_joiners=true,
  )
  sun_codepoint_is_hebrew_script(representative) ||
  sun_codepoint_is_arabic_script(representative)
}

///|
fn sun_text_segment_count(clusters : Array[String]) -> Int {
  if clusters.is_empty() {
    return 0
  }
  let mut count = 0
  let mut current_class = -1
  for cluster in clusters {
    let cluster_class = sun_text_segment_class(cluster)
    if count == 0 || cluster_class != current_class {
      count = count + 1
      current_class = cluster_class
    }
  }
  count
}

///|
fn sun_text_segment_class(cluster : String) -> Int {
  let representative = @render_common.representative_fallback_codepoint(
    cluster,
    skip_joiners=true,
  )
  if @render_common.codepoint_is_emoji_hint(representative) {
    1
  } else if @render_common.codepoint_is_cjk_ideograph(representative) ||
    @render_common.codepoint_is_kana(representative) {
    2
  } else if @render_common.codepoint_is_hangul_cluster_codepoint(representative) {
    3
  } else if sun_codepoint_is_hebrew_script(representative) ||
    sun_codepoint_is_arabic_script(representative) {
    4
  } else if representative < 0x0300 {
    0
  } else {
    5
  }
}

///|
fn sun_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)
}

///|
fn sun_text_cluster_arrays_equal(
  left : Array[String],
  right : Array[String],
) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for index in 0.. Array[@core.Rect] {
  let boundaries = @core.TextGraphemeBoundaries::new(text~)
  let stabilized = caret_rects.copy()
  for index in 0..= 0 && nearest < caret_rects.length() {
      stabilized[index] = caret_rects[nearest]
    }
  }
  stabilized
}