///|
pub struct SkiaTextVisualOrderDiagnostic {
  priv logical_text : String
  priv visual_text : String
  priv logical_cluster_count : Int
  priv visual_cluster_count : Int
  priv segment_count : Int
  priv glyph_count : Int
  priv width : Double
  priv visual_order_differs : Bool
} derive(Eq, Debug, ToJson)

///|
pub struct SkiaTextSelectionGeometryDiagnostic {
  priv text : String
  priv selection_range : @core.TextRange
  priv selection_rect_count : Int
  priv selected_line_count : Int
  priv line_count : Int
  priv hit_test_offset : Int
  priv hit_test_line_index : Int
  priv hit_test_inside : Bool
  priv visual_order_differs : Bool
  priv visual_segment_count : Int
  priv bidi_visual_order_observed : Bool
} derive(Eq, Debug, ToJson)

///|
pub struct SkiaEmojiFontFallbackDiagnostic {
  priv text : String
  priv text_system_id : String
  priv font_family : String
  priv font_source : String
  priv resolved_font_name : String
  priv fallback_language_tags : Array[String]
  priv fallback_script_tag : String
  priv fallback_language_tag_count : Int
  priv fallback_request_language_count : Int
  priv shaper : String
  priv glyph_format : String
  priv glyph_key : String
  priv glyph_count : Int
  priv cluster_count : Int
  priv glyph_width : Int
  priv glyph_height : Int
  priv resolved_missing_glyph_count : Int
  priv fallback_character : Int
  priv emoji_hint : Bool
  priv emoji_fallback_attempted : Bool
  priv fallback_request_ready : Bool
  priv missing_glyph_recovery_ready : Bool
  priv deterministic_color_emoji_ready : Bool
} derive(Eq, Debug, ToJson)

///|
pub fn SkiaTextVisualOrderDiagnostic::logical_text(
  self : SkiaTextVisualOrderDiagnostic,
) -> String {
  self.logical_text
}

///|
pub fn SkiaTextVisualOrderDiagnostic::visual_text(
  self : SkiaTextVisualOrderDiagnostic,
) -> String {
  self.visual_text
}

///|
pub fn SkiaTextVisualOrderDiagnostic::logical_cluster_count(
  self : SkiaTextVisualOrderDiagnostic,
) -> Int {
  self.logical_cluster_count
}

///|
pub fn SkiaTextVisualOrderDiagnostic::visual_cluster_count(
  self : SkiaTextVisualOrderDiagnostic,
) -> Int {
  self.visual_cluster_count
}

///|
pub fn SkiaTextVisualOrderDiagnostic::segment_count(
  self : SkiaTextVisualOrderDiagnostic,
) -> Int {
  self.segment_count
}

///|
pub fn SkiaTextVisualOrderDiagnostic::glyph_count(
  self : SkiaTextVisualOrderDiagnostic,
) -> Int {
  self.glyph_count
}

///|
pub fn SkiaTextVisualOrderDiagnostic::width(
  self : SkiaTextVisualOrderDiagnostic,
) -> Double {
  self.width
}

///|
pub fn SkiaTextVisualOrderDiagnostic::visual_order_differs(
  self : SkiaTextVisualOrderDiagnostic,
) -> Bool {
  self.visual_order_differs
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::text(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> String {
  self.text
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::selection_range(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> @core.TextRange {
  self.selection_range
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::selection_rect_count(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Int {
  self.selection_rect_count
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::selected_line_count(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Int {
  self.selected_line_count
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::line_count(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Int {
  self.line_count
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::hit_test_offset(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Int {
  self.hit_test_offset
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::hit_test_line_index(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Int {
  self.hit_test_line_index
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::hit_test_inside(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Bool {
  self.hit_test_inside
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::visual_order_differs(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Bool {
  self.visual_order_differs
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::visual_segment_count(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Int {
  self.visual_segment_count
}

///|
pub fn SkiaTextSelectionGeometryDiagnostic::bidi_visual_order_observed(
  self : SkiaTextSelectionGeometryDiagnostic,
) -> Bool {
  self.bidi_visual_order_observed
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::text(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.text
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::text_system_id(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.text_system_id
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::font_family(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.font_family
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::font_source(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.font_source
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::resolved_font_name(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.resolved_font_name
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::fallback_language_tags(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Array[String] {
  self.fallback_language_tags
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::fallback_script_tag(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.fallback_script_tag
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::fallback_language_tag_count(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.fallback_language_tag_count
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::fallback_request_language_count(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.fallback_request_language_count
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::shaper(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.shaper
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::glyph_format(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.glyph_format
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::glyph_key(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> String {
  self.glyph_key
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::glyph_count(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.glyph_count
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::cluster_count(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.cluster_count
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::glyph_width(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.glyph_width
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::glyph_height(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.glyph_height
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::resolved_missing_glyph_count(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.resolved_missing_glyph_count
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::fallback_character(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Int {
  self.fallback_character
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::emoji_hint(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Bool {
  self.emoji_hint
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::emoji_fallback_attempted(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Bool {
  self.emoji_fallback_attempted
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::fallback_request_ready(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Bool {
  self.fallback_request_ready
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::missing_glyph_recovery_ready(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Bool {
  self.missing_glyph_recovery_ready
}

///|
pub fn SkiaEmojiFontFallbackDiagnostic::deterministic_color_emoji_ready(
  self : SkiaEmojiFontFallbackDiagnostic,
) -> Bool {
  self.deterministic_color_emoji_ready
}

///|
pub fn skia_text_visual_order_diagnostic(
  text : String,
  font? : @core.FontSpec = @core.FontSpec::new(size=20.0, weight=500),
  font_resolution? : SkiaFontResolution = SkiaFontResolution::SystemFontMgr,
) -> SkiaTextVisualOrderDiagnostic? {
  if text == "" {
    return None
  }
  let logical_clusters = @render_common.text_grapheme_clusters(text)
  let visual_clusters = @render_common.text_bidi_visual_clusters(text)
  match skia_resolved_text_layout_for_resolution(font, text, font_resolution) {
    Some(layout) => {
      let mut glyph_count = 0
      for segment in layout.segments {
        glyph_count += segment.glyph_run.glyphs.length()
      }
      Some({
        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: layout.segments.length(),
        glyph_count,
        width: layout.width,
        visual_order_differs: !@render_common.text_string_arrays_equal(
          logical_clusters, visual_clusters,
        ),
      })
    }
    None => None
  }
}

///|
pub fn skia_text_selection_geometry_diagnostic(
  text : String,
  selection? : @core.TextRange? = None,
  font? : @core.FontSpec = @core.FontSpec::new(size=20.0, weight=500),
  max_width? : Double = 96.0,
  font_resolution? : SkiaFontResolution = SkiaFontResolution::SystemFontMgr,
) -> SkiaTextSelectionGeometryDiagnostic? {
  if text == "" {
    return None
  }
  let system = skia_text_system(resolution=font_resolution)
  let layout = system.layout_paragraph(
    @core.TextLayoutInput::new(text~, font~, max_width~),
  )
  if layout.lines.is_empty() {
    return None
  }
  let selection_range = match selection {
    Some(range) => range
    None => skia_default_selection_geometry_range(layout)
  }
  let normalized = @core.normalize_grapheme_range(
    @core.TextGraphemeBoundaries::new(text~),
    selection_range,
  )
  let selection_rects = layout.selection_rects(normalized)
  let hit_line = if layout.lines.length() > 1 {
    layout.lines[1]
  } else {
    layout.lines[0]
  }
  let hit = layout.hit_test(
    @core.Point::new(
      x=(hit_line.origin.x + hit_line.size.width * 0.5).min(max_width),
      y=hit_line.origin.y + hit_line.size.height * 0.5,
    ),
  )
  let visual = skia_text_visual_order_diagnostic(text, font~, font_resolution~)
  let (visual_order_differs, visual_segment_count, bidi_visual_order_observed) = match
    visual {
    Some(diagnostic) =>
      (
        diagnostic.visual_order_differs,
        diagnostic.segment_count,
        diagnostic.visual_order_differs &&
        diagnostic.logical_cluster_count == diagnostic.visual_cluster_count &&
        diagnostic.segment_count >= 3 &&
        diagnostic.glyph_count >= 1,
      )
    None => (false, 0, false)
  }
  Some({
    text,
    selection_range: normalized,
    selection_rect_count: selection_rects.length(),
    selected_line_count: skia_selected_line_count(layout, normalized),
    line_count: layout.line_count(),
    hit_test_offset: hit.offset,
    hit_test_line_index: hit.line_index,
    hit_test_inside: hit.is_inside,
    visual_order_differs,
    visual_segment_count,
    bidi_visual_order_observed,
  })
}

///|
pub fn skia_emoji_font_fallback_diagnostic(
  text? : String = "\u{1F469}\u{200D}\u{1F4BB}",
  font? : @core.FontSpec = @core.FontSpec::new(
    families=@core.FontFamilyStack::new([@core.FontFamily::Emoji]),
    size=32.0,
    weight=400,
  ),
  font_resolution? : SkiaFontResolution = SkiaFontResolution::SystemFontMgr,
) -> SkiaEmojiFontFallbackDiagnostic? {
  if text == "" {
    return None
  }
  let emoji_hint = skia_text_contains_emoji_hint(text)
  let fallback_request = skia_font_fallback_request(font, text)
  let fallback_language_tags = skia_font_fallback_language_tags(text)
  let fallback_script_tag = skia_fallback_script_tag(fallback_language_tags)
  let fallback_request_language_count = fallback_request.language_count()
  match skia_resolved_text_layout_for_resolution(font, text, font_resolution) {
    Some(layout) => {
      let glyph_count = skia_resolved_text_layout_glyph_count(layout)
      if glyph_count <= 0 || layout.width <= 0.0 || layout.metrics.height <= 0.0 {
        return None
      }
      let text_system = skia_text_system(resolution=font_resolution)
      let shaper = if @skia_native.skia_shaper_available() {
        "skshaper"
      } else {
        "fallback-glyph-run"
      }
      let fallback_character = skia_representative_fallback_codepoint(text)
      let font_source = skia_emoji_font_source_label(font_resolution)
      let glyph_format = skia_emoji_glyph_format_label()
      let resolved_missing_glyph_count = skia_resolved_text_layout_missing_glyph_count(
        layout,
      )
      Some({
        text,
        text_system_id: text_system.id(),
        font_family: "emoji",
        font_source,
        resolved_font_name: skia_emoji_resolved_font_name(),
        fallback_language_tags,
        fallback_script_tag,
        fallback_language_tag_count: fallback_language_tags.length(),
        fallback_request_language_count,
        shaper,
        glyph_format,
        glyph_key: skia_emoji_glyph_key(
          font_source,
          text_system.id(),
          shaper,
          fallback_script_tag,
          fallback_language_tags,
          fallback_request_language_count,
          fallback_character,
          glyph_format,
        ),
        glyph_count,
        cluster_count: @render_common.text_grapheme_clusters(text).length(),
        glyph_width: skia_positive_pixel_extent(layout.width),
        glyph_height: skia_positive_pixel_extent(layout.metrics.height),
        resolved_missing_glyph_count,
        fallback_character,
        emoji_hint,
        emoji_fallback_attempted: font_resolution ==
        SkiaFontResolution::SystemFontMgr &&
        emoji_hint,
        fallback_request_ready: !fallback_request.is_empty() &&
        fallback_request.has_character() &&
        fallback_request_language_count == fallback_language_tags.length(),
        missing_glyph_recovery_ready: skia_sample_missing_glyph_recovery_ready(),
        deterministic_color_emoji_ready: skia_emoji_typeface_has_color_glyphs(),
      })
    }
    None => None
  }
}

///|
fn skia_emoji_font_source_label(resolution : SkiaFontResolution) -> String {
  match resolution {
    SkiaFontResolution::SystemFontMgr => "skia-system-fontmgr"
    SkiaFontResolution::EmptyTypeface => "skia-empty-typeface"
  }
}

///|
fn skia_emoji_typeface_has_color_glyphs() -> Bool {
  match @skia_native.FontMgr::default() {
    None => false
    Some(mgr) =>
      match mgr.match_family_style_character(b"", [], 0x1F469) {
        None => false
        Some(typeface) => typeface.has_color_glyphs()
      }
  }
}

///|
fn skia_emoji_glyph_format_label() -> String {
  if skia_emoji_typeface_has_color_glyphs() {
    "rgba"
  } else {
    "alpha"
  }
}

///|
fn skia_emoji_resolved_font_name() -> String {
  match @skia_native.FontMgr::default() {
    None => "no-fontmgr"
    Some(mgr) =>
      match mgr.match_family_style_character(b"", [], 0x1F469) {
        None => "no-match"
        Some(typeface) =>
          match typeface.family_name() {
            None => "unnamed"
            Some(name) => name.to_string()
          }
      }
  }
}

///|
fn skia_fallback_script_tag(tags : Array[String]) -> String {
  if tags.is_empty() {
    ""
  } else {
    tags[0]
  }
}

///|
fn skia_emoji_glyph_key(
  font_source : String,
  text_system_id : String,
  shaper : String,
  fallback_script_tag : String,
  fallback_language_tags : Array[String],
  fallback_request_language_count : Int,
  fallback_character : Int,
  glyph_format : String,
) -> String {
  let script_key = if fallback_script_tag == "" {
    "script=und"
  } else {
    "script=\{fallback_script_tag}"
  }
  let language_key = if fallback_language_tags.is_empty() {
    "lang-tags=none"
  } else {
    "lang-tags=\{fallback_language_tags.join("+")}"
  }
  "\{font_source}|\{text_system_id}|\{shaper}|\{script_key}|langs=\{fallback_request_language_count}|\{language_key}|emoji-u+\{fallback_character}|\{glyph_format}"
}

///|
fn skia_resolved_text_layout_glyph_count(
  layout : SkiaResolvedTextLayout,
) -> Int {
  let mut glyph_count = 0
  for segment in layout.segments {
    glyph_count += segment.glyph_run.glyphs.length()
  }
  glyph_count
}

///|
fn skia_resolved_text_layout_missing_glyph_count(
  layout : SkiaResolvedTextLayout,
) -> Int {
  let mut missing_glyph_count = 0
  for segment in layout.segments {
    missing_glyph_count += skia_glyph_run_missing_glyph_count(segment.glyph_run)
  }
  missing_glyph_count
}

///|
fn skia_positive_pixel_extent(value : Double) -> Int {
  value.ceil().to_int().max(1)
}

///|
fn skia_default_selection_geometry_range(
  layout : @core.TextParagraphLayoutResult,
) -> @core.TextRange {
  if layout.lines.length() > 1 {
    @core.TextRange::new(
      start=layout.lines[0].text_range.start,
      end=layout.lines[1].text_range.end,
    )
  } else {
    @core.TextRange::new(start=0, end=layout.text.to_array().length())
  }
}

///|
fn skia_selected_line_count(
  layout : @core.TextParagraphLayoutResult,
  range : @core.TextRange,
) -> Int {
  let range = range.normalized()
  if range.is_collapsed() {
    return 1
  }
  let mut count = 0
  for line in layout.lines {
    if range.start < line.text_range.end && range.end > line.text_range.start {
      count += 1
    }
  }
  count
}