///|
priv struct SkiaParagraphClusterBox {
  has_rect : Bool
  rtl : Bool
}

///|
pub fn SkiaRasterRenderer::text_system(
  self : SkiaRasterRenderer,
) -> @core.TextSystem {
  skia_text_system(resolution=self.font_resolution)
}

///|
pub fn skia_text_system(
  resolution? : SkiaFontResolution = SkiaFontResolution::SystemFontMgr,
) -> @core.TextSystem {
  match resolution {
    SkiaFontResolution::EmptyTypeface =>
      if skia_available() {
        skia_empty_typeface_text_system()
      } else {
        @core.TextSystem::fallback()
      }
    SkiaFontResolution::SystemFontMgr => skia_system_font_mgr_text_system()
  }
}

///|
fn skia_empty_typeface_text_system() -> @core.TextSystem {
  @core.TextSystem::new(
    id="skia-raster-empty-typeface-text-system",
    measure=input => {
      skia_measure_text_for_resolution(input, SkiaFontResolution::EmptyTypeface)
    },
    paragraph_layout=Some(input => {
      skia_fallback_paragraph_layout_result(
        input,
        SkiaFontResolution::EmptyTypeface,
        "skia-raster-empty-typeface-text-system",
      )
    }),
  )
}

///|
fn skia_system_font_mgr_text_system() -> @core.TextSystem {
  @core.TextSystem::new(
    id="skia-raster-text-system",
    measure=input => {
      skia_measure_text_for_resolution(input, SkiaFontResolution::SystemFontMgr)
    },
    paragraph_layout=Some(input => {
      match skia_native_paragraph_layout_result(input) {
        Some(layout) => layout
        None =>
          skia_fallback_paragraph_layout_result(
            input,
            SkiaFontResolution::SystemFontMgr,
            "skia-raster-text-system",
          )
      }
    }),
    register_font_data=skia_register_font_data,
  )
}

///|
fn skia_measure_text_for_resolution(
  input : @core.TextLayoutInput,
  resolution : SkiaFontResolution,
) -> @core.TextLayoutResult {
  let fallback = @core.TextSystem::fallback().measure_text(input)
  match
    skia_resolved_text_layout_for_resolution(input.font, input.text, resolution) {
    Some(layout) => skia_resolved_text_layout_result(input, fallback, layout~)
    None => fallback
  }
}

///|
fn skia_fallback_paragraph_layout_result(
  input : @core.TextLayoutInput,
  resolution : SkiaFontResolution,
  id : String,
) -> @core.TextParagraphLayoutResult {
  @core.TextSystem::new(id~, measure=measure_input => {
    skia_measure_text_for_resolution(measure_input, resolution)
  }).layout_paragraph(input)
}

///|
fn skia_native_paragraph_layout_result(
  input : @core.TextLayoutInput,
) -> @core.TextParagraphLayoutResult? {
  if input.text == "" || !@skia_native.skia_paragraph_available() {
    return None
  }
  let max_width = input.max_width.unwrap_or(1000000000.0)
  let family_name = skia_primary_paragraph_family(input.font)
  let text_bytes = @utf8.encode(input.text[:], bom=false)
  match
    @skia_native.Paragraph::layout_utf8(
      text_bytes,
      family_name~,
      size=Float::from_double(input.font.size),
      weight=input.font.weight,
      width=5,
      slant=skia_font_slant(input.font.style),
      max_width=Float::from_double(max_width),
      left_to_right=true,
    ) {
    None => None
    Some(paragraph) => {
      let offsets = skia_utf8_char_offsets(input.text)
      let lines = skia_paragraph_line_metrics(input.text, paragraph, offsets)
      if lines.is_empty() {
        return None
      }
      let caret_rects = skia_paragraph_caret_rects(
        input.text,
        paragraph,
        lines,
        offsets,
      )
      if caret_rects.length() != input.text.to_array().length() + 1 {
        return None
      }
      let all_text_boxes = paragraph.text_boxes_utf8(0, text_bytes.length())
      if all_text_boxes.is_empty() {
        return None
      }
      let probe = skia_paragraph_bidi_probe_ready(input.font)
      let visual_order = skia_paragraph_visual_order_metadata(
        input.text,
        paragraph,
        offsets,
        bidi_ready=probe,
      )
      let hit = paragraph.hit_test_utf8(
        0.0,
        Float::from_double(lines[0].baseline),
      )
      let hit_available = hit.offset >= 0 && hit.offset <= text_bytes.length()
      let selection_rects_available = all_text_boxes.length() > 0
      let line_metrics_available = !lines.is_empty()
      let native_ready = line_metrics_available &&
        selection_rects_available &&
        hit_available &&
        !caret_rects.is_empty()
      Some({
        text: input.text,
        size: @core.Size::new(
          width=paragraph.width().to_double().min(max_width),
          height=paragraph.height().to_double(),
        ),
        baseline: lines[0].baseline,
        lines,
        caret_rects,
        visual_order,
        metadata: {
          text_system_id: "skia-raster-text-system",
          paragraph_layout_available: true,
          line_metrics_available,
          selection_rects_available,
          hit_test_available: hit_available,
          native_paragraph_ready: native_ready,
          bidi_visual_order_ready: native_ready && probe,
        },
      })
    }
  }
}

///|
fn skia_primary_paragraph_family(font : @core.FontSpec) -> Bytes {
  let candidates = skia_preflight_family_candidates(font)
  if candidates.is_empty() {
    b""
  } else {
    @utf8.encode(candidates[0][:], bom=false)
  }
}

///|
fn skia_paragraph_line_metrics(
  text : String,
  paragraph : @skia_native.Paragraph,
  offsets : Array[Int],
) -> Array[@core.TextParagraphLineMetrics] {
  let chars = text.to_array()
  let lines : Array[@core.TextParagraphLineMetrics] = []
  for metric in paragraph.line_metrics() {
    match
      (
        skia_char_index_for_utf8_offset(offsets, metric.start),
        skia_char_index_for_utf8_offset(offsets, metric.end),
      ) {
      (Some(start), Some(end)) => {
        let start = start.clamp(min=0, max=chars.length())
        let end = end.clamp(min=start, max=chars.length())
        let ascent = metric.ascent.to_double().abs()
        let descent = metric.descent.to_double().abs()
        let metric_height = metric.height
          .to_double()
          .max(ascent + descent)
          .max(1.0)
        let top = metric.baseline.to_double() - ascent
        lines.push({
          line_index: lines.length(),
          text_range: @core.TextRange::new(start~, end~),
          origin: @core.Point::new(x=metric.left.to_double(), y=top.max(0.0)),
          size: @core.Size::new(
            width=metric.width.to_double().max(0.0),
            height=metric_height,
          ),
          baseline: metric.baseline.to_double(),
        })
      }
      _ => ()
    }
  }
  lines
}

///|
fn skia_paragraph_caret_rects(
  text : String,
  paragraph : @skia_native.Paragraph,
  lines : Array[@core.TextParagraphLineMetrics],
  offsets : Array[Int],
) -> Array[@core.Rect] {
  let chars = text.to_array()
  let carets : Array[@core.Rect] = []
  for index in 0..<(chars.length() + 1) {
    carets.push(skia_paragraph_caret_rect_at(paragraph, lines, offsets, index))
  }
  carets
}

///|
fn skia_paragraph_caret_rect_at(
  paragraph : @skia_native.Paragraph,
  lines : Array[@core.TextParagraphLineMetrics],
  offsets : Array[Int],
  index : Int,
) -> @core.Rect {
  let line = skia_paragraph_line_for_char_index(lines, index)
  let start_byte = offsets[index.clamp(min=0, max=offsets.length() - 1)]
  let end_byte = if index + 1 < offsets.length() {
    offsets[index + 1]
  } else {
    start_byte
  }
  let boxes = if end_byte > start_byte {
    paragraph.text_boxes_utf8(start_byte, end_byte)
  } else if start_byte > 0 {
    paragraph.text_boxes_utf8(offsets[(index - 1).max(0)], start_byte)
  } else {
    []
  }
  let x = if boxes.is_empty() {
    line.origin.x
  } else if end_byte > start_byte {
    boxes[0].left.to_double()
  } else {
    boxes[boxes.length() - 1].right.to_double()
  }
  @core.Rect::new(
    x~,
    y=line.origin.y,
    width=1.0,
    height=line.size.height.max(1.0),
  )
}

///|
fn skia_paragraph_line_for_char_index(
  lines : Array[@core.TextParagraphLineMetrics],
  index : Int,
) -> @core.TextParagraphLineMetrics {
  if lines.is_empty() {
    return {
      line_index: 0,
      text_range: @core.TextRange::collapsed(0),
      origin: @core.Point::new(x=0.0, y=0.0),
      size: @core.Size::new(width=0.0, height=1.0),
      baseline: 0.0,
    }
  }
  for line in lines {
    if index >= line.text_range.start && index <= line.text_range.end {
      return line
    }
  }
  lines[lines.length() - 1]
}

///|
fn skia_paragraph_visual_order_metadata(
  text : String,
  paragraph : @skia_native.Paragraph,
  offsets : Array[Int],
  bidi_ready~ : Bool,
) -> @core.TextParagraphVisualOrderMetadata {
  let logical_clusters = @render_common.text_grapheme_clusters(text)
  let boxes = skia_paragraph_cluster_boxes(logical_clusters, paragraph, offsets)
  let paragraph_bidi_observed = skia_paragraph_has_bidi_observation(boxes)
  let visual_clusters = if paragraph_bidi_observed {
    @render_common.text_bidi_visual_clusters(text)
  } else {
    logical_clusters
  }
  let visual_order_differs = !@render_common.text_string_arrays_equal(
    logical_clusters, visual_clusters,
  )
  {
    logical_text: @render_common.text_join_clusters(logical_clusters),
    visual_text: @render_common.text_join_clusters(visual_clusters),
    logical_cluster_count: logical_clusters.length(),
    visual_cluster_count: visual_clusters.length(),
    segment_count: boxes.length().max(1),
    glyph_count: logical_clusters.length(),
    visual_order_differs,
    bidi_visual_order_ready: bidi_ready &&
    paragraph_bidi_observed &&
    visual_order_differs,
  }
}

///|
fn skia_paragraph_cluster_boxes(
  clusters : Array[String],
  paragraph : @skia_native.Paragraph,
  offsets : Array[Int],
) -> Array[SkiaParagraphClusterBox] {
  let boxes : Array[SkiaParagraphClusterBox] = []
  let mut char_index = 0
  for cluster in clusters {
    let cluster_length = cluster.to_array().length()
    let end_index = (char_index + cluster_length).min(offsets.length() - 1)
    if end_index > char_index {
      let text_boxes = paragraph.text_boxes_utf8(
        offsets[char_index],
        offsets[end_index],
      )
      let has_rect = !text_boxes.is_empty()
      let native_rtl = if has_rect {
        let directions = paragraph.text_box_directions_utf8(
          offsets[char_index],
          offsets[end_index],
        )
        if directions.is_empty() {
          false
        } else {
          directions[0] == 0
        }
      } else {
        false
      }
      boxes.push({
        has_rect,
        rtl: native_rtl || @render_common.text_segment_class(cluster) == 4,
      })
    }
    char_index = end_index
  }
  boxes
}

///|
fn skia_paragraph_has_bidi_observation(
  boxes : Array[SkiaParagraphClusterBox],
) -> Bool {
  let mut rect_count = 0
  let mut rtl_count = 0
  for box in boxes {
    if box.has_rect {
      rect_count = rect_count + 1
    }
    if box.rtl {
      rtl_count = rtl_count + 1
    }
  }
  rect_count > 0 && rtl_count > 0
}

///|
fn skia_paragraph_bidi_probe_ready(font : @core.FontSpec) -> Bool {
  if !@skia_native.skia_paragraph_available() {
    return false
  }
  let text = "ABC \u{05D0}\u{05D1}\u{05D2} 123"
  let bytes = @utf8.encode(text[:], bom=false)
  match
    @skia_native.Paragraph::layout_utf8(
      bytes,
      family_name=skia_primary_paragraph_family(font),
      size=Float::from_double(font.size),
      weight=font.weight,
      width=5,
      slant=skia_font_slant(font.style),
      max_width=400.0,
      left_to_right=true,
    ) {
    Some(paragraph) => {
      let offsets = skia_utf8_char_offsets(text)
      let logical = @render_common.text_grapheme_clusters(text)
      let visual = skia_paragraph_visual_order_metadata(
        text,
        paragraph,
        offsets,
        bidi_ready=true,
      )
      visual.visual_order_differs &&
      visual.bidi_visual_order_ready &&
      visual.logical_cluster_count == logical.length() &&
      visual.visual_cluster_count == logical.length()
    }
    None => false
  }
}