///|
pub(all) struct TextLayoutInput {
  text : String
  font : FontSpec
  max_width : Double?
} derive(Eq, Debug, ToJson)

///|
pub(all) struct TextLayoutResult {
  size : Size
  baseline : Double
  caret_positions : Array[Double]
} derive(Eq, Debug, ToJson)

///|
pub(all) struct TextParagraphLineMetrics {
  line_index : Int
  text_range : TextRange
  origin : Point
  size : Size
  baseline : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct TextParagraphVisualOrderMetadata {
  logical_text : String
  visual_text : String
  logical_cluster_count : Int
  visual_cluster_count : Int
  segment_count : Int
  glyph_count : Int
  visual_order_differs : Bool
  bidi_visual_order_ready : Bool
} derive(Eq, Debug, ToJson)

///|
pub(all) struct TextParagraphLayoutMetadata {
  text_system_id : String
  paragraph_layout_available : Bool
  line_metrics_available : Bool
  selection_rects_available : Bool
  hit_test_available : Bool
  native_paragraph_ready : Bool
  bidi_visual_order_ready : Bool
} derive(Eq, Debug, ToJson)

///|
pub(all) struct TextParagraphHitTestResult {
  offset : Int
  line_index : Int
  caret_rect : Rect
  is_inside : Bool
} derive(Eq, Debug, ToJson)

///|
pub(all) struct TextParagraphLayoutResult {
  text : String
  size : Size
  baseline : Double
  lines : Array[TextParagraphLineMetrics]
  caret_rects : Array[Rect]
  visual_order : TextParagraphVisualOrderMetadata
  metadata : TextParagraphLayoutMetadata
} derive(Eq, Debug, ToJson)

///|
pub struct TextSystem {
  id : String
  measure : (TextLayoutInput) -> TextLayoutResult
  paragraph_layout : (TextLayoutInput) -> TextParagraphLayoutResult
  register_font_data : (FontFamily, Bytes) -> Unit
}

///|
pub fn TextLayoutInput::new(
  text~ : String,
  font~ : FontSpec,
  max_width? : Double,
) -> TextLayoutInput {
  { text, font, max_width }
}

///|
pub fn TextSystem::new(
  id~ : String,
  measure~ : (TextLayoutInput) -> TextLayoutResult,
  paragraph_layout? : ((TextLayoutInput) -> TextParagraphLayoutResult)? = None,
  register_font_data? : (FontFamily, Bytes) -> Unit = (_family, _data) => (),
) -> TextSystem {
  let paragraph_layout = match paragraph_layout {
    Some(layout) => layout
    None => input => fallback_paragraph_layout_result(input, measure, id)
  }
  { id, measure, paragraph_layout, register_font_data }
}

///|
pub fn TextSystem::fallback() -> TextSystem {
  TextSystem::new(
    id="core-fallback-text-system",
    measure=fallback_measure_text_layout,
  )
}

///|
pub fn TextSystem::id(self : TextSystem) -> String {
  self.id
}

///|
pub fn TextSystem::measure_text(
  self : TextSystem,
  input : TextLayoutInput,
) -> TextLayoutResult {
  (self.measure)(input)
}

///|
pub fn TextSystem::layout_paragraph(
  self : TextSystem,
  input : TextLayoutInput,
) -> TextParagraphLayoutResult {
  (self.paragraph_layout)(input)
}

///|
pub fn TextSystem::register_font_data(
  self : TextSystem,
  family : FontFamily,
  data : Bytes,
) -> Unit {
  (self.register_font_data)(family, data)
}

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

///|
pub fn TextParagraphLayoutResult::caret_rect_at(
  self : TextParagraphLayoutResult,
  offset : Int,
) -> Rect {
  if self.caret_rects.is_empty() {
    return Rect::new(x=0.0, y=0.0, width=1.0, height=0.0)
  }
  let boundaries = TextGraphemeBoundaries::new(text=self.text)
  let offset = boundaries.nearest_boundary(offset)
  self.caret_rects[clamp_int(offset, 0, self.caret_rects.length() - 1)]
}

///|
pub fn TextParagraphLayoutResult::selection_rects(
  self : TextParagraphLayoutResult,
  range : TextRange,
) -> Array[Rect] {
  let boundaries = TextGraphemeBoundaries::new(text=self.text)
  let normalized = normalize_grapheme_range(boundaries, range)
  if normalized.is_collapsed() {
    return [self.caret_rect_at(normalized.start)]
  }
  let rects : Array[Rect] = []
  for line in self.lines {
    let start = max_int(normalized.start, line.text_range.start)
    let end = min_int(normalized.end, line.text_range.end)
    if start < end {
      let start_rect = self.caret_rect_at(start)
      let end_rect = self.caret_rect_at(end)
      let x0 = start_rect.origin.x.min(end_rect.origin.x)
      let x1 = start_rect.origin.x.max(end_rect.origin.x)
      rects.push(
        Rect::new(
          x=x0,
          y=line.origin.y,
          width=(x1 - x0).max(1.0),
          height=line.size.height,
        ),
      )
    }
  }
  rects
}

///|
pub fn TextParagraphLayoutResult::hit_test(
  self : TextParagraphLayoutResult,
  point : Point,
) -> TextParagraphHitTestResult {
  let line = self.closest_line_for_point(point)
  let mut best_offset = line.text_range.start
  let mut best_distance = 1.0e18
  let hit_end = line.text_range.end + 1
  for offset in line.text_range.start..= rect.origin.x {
      point.x - rect.origin.x
    } else {
      rect.origin.x - point.x
    }
    if distance < best_distance {
      best_distance = distance
      best_offset = offset
    }
  }
  let boundaries = TextGraphemeBoundaries::new(text=self.text)
  best_offset = boundaries.nearest_boundary(best_offset)
  let caret_rect = self.caret_rect_at(best_offset)
  {
    offset: best_offset,
    line_index: line.line_index,
    caret_rect,
    is_inside: point.x >= 0.0 &&
    point.y >= 0.0 &&
    point.x <= self.size.width &&
    point.y <= self.size.height,
  }
}

///|
fn TextParagraphLayoutResult::closest_line_for_point(
  self : TextParagraphLayoutResult,
  point : Point,
) -> TextParagraphLineMetrics {
  if self.lines.is_empty() {
    return {
      line_index: 0,
      text_range: TextRange::collapsed(0),
      origin: Point::new(x=0.0, y=0.0),
      size: Size::new(width=0.0, height=0.0),
      baseline: 0.0,
    }
  }
  let mut closest = self.lines[0]
  let mut closest_distance = 1.0e18
  for line in self.lines {
    let line_top = line.origin.y
    let line_bottom = line.origin.y + line.size.height
    let distance = if point.y < line_top {
      line_top - point.y
    } else if point.y > line_bottom {
      point.y - line_bottom
    } else {
      0.0
    }
    if distance < closest_distance {
      closest = line
      closest_distance = distance
    }
  }
  closest
}

///|
fn fallback_measure_text_layout(input : TextLayoutInput) -> TextLayoutResult {
  let raw = fallback_text_layout_result(input)
  match input.max_width {
    Some(max_width) => clamp_text_layout_result(raw, max_width)
    None => raw
  }
}

///|
fn clamp_text_layout_result(
  result : TextLayoutResult,
  max_width : Double,
) -> TextLayoutResult {
  let carets : Array[Double] = []
  for caret in result.caret_positions {
    carets.push(min_double(caret, max_width))
  }
  {
    size: Size::new(
      width=min_double(result.size.width, max_width),
      height=result.size.height,
    ),
    baseline: result.baseline,
    caret_positions: carets,
  }
}

///|
fn fallback_paragraph_layout_result(
  input : TextLayoutInput,
  measure : (TextLayoutInput) -> TextLayoutResult,
  text_system_id : String,
) -> TextParagraphLayoutResult {
  let measured = measure(TextLayoutInput::new(text=input.text, font=input.font))
  let line_ranges = fallback_paragraph_line_ranges(input, measured)
  let caret_rects : Array[Rect] = []
  let text_length = input.text.to_array().length()
  let caret_count = text_length + 1
  for _ in 0.. Array[TextRange] {
  let chars = input.text.to_array()
  let length = chars.length()
  if length == 0 {
    return [TextRange::collapsed(0)]
  }
  let boundaries = TextGraphemeBoundaries::new(text=input.text)
  let ranges : Array[TextRange] = []
  let mut line_start = 0
  let mut cursor = 0
  let mut last_soft_break = -1
  while cursor < length {
    let next = boundaries.next_boundary(cursor)
    let cluster = String::from_array(chars[cursor:next])
    if cluster == "\n" {
      ranges.push(TextRange::new(start=line_start, end=cursor))
      line_start = next
      cursor = next
      last_soft_break = -1
    } else {
      let candidate_width = fallback_paragraph_caret_span_width(
        measured, line_start, next,
      )
      if fallback_paragraph_should_wrap(input.max_width, candidate_width) &&
        line_start < cursor {
        let break_end = if last_soft_break > line_start {
          last_soft_break
        } else {
          cursor
        }
        ranges.push(TextRange::new(start=line_start, end=break_end))
        line_start = break_end
        cursor = line_start
        last_soft_break = -1
      } else {
        if fallback_paragraph_cluster_is_soft_break(cluster) {
          last_soft_break = next
        }
        cursor = next
      }
    }
  }
  ranges.push(TextRange::new(start=line_start, end=length))
  ranges
}

///|
fn fallback_paragraph_range_measure(
  measured : TextLayoutResult,
  range : TextRange,
  font : FontSpec,
) -> TextLayoutResult {
  let origin = fallback_paragraph_caret_position(measured, range.start)
  let carets : Array[Double] = []
  for offset in range.start..<=range.end {
    carets.push(
      max_double(
        0.0,
        fallback_paragraph_caret_position(measured, offset) - origin,
      ),
    )
  }
  {
    size: Size::new(
      width=fallback_paragraph_caret_span_width(
        measured,
        range.start,
        range.end,
      ),
      height=measured.size.height.max(font.size * 1.25),
    ),
    baseline: measured.baseline,
    caret_positions: carets,
  }
}

///|
fn fallback_paragraph_caret_span_width(
  measured : TextLayoutResult,
  start : Int,
  end : Int,
) -> Double {
  max_double(
    0.0,
    fallback_paragraph_caret_position(measured, end) -
    fallback_paragraph_caret_position(measured, start),
  )
}

///|
fn fallback_paragraph_caret_position(
  measured : TextLayoutResult,
  offset : Int,
) -> Double {
  if measured.caret_positions.is_empty() {
    return 0.0
  }
  measured.caret_positions[clamp_int(
    offset,
    0,
    measured.caret_positions.length() - 1,
  )]
}

///|
fn fallback_paragraph_should_wrap(
  max_width : Double?,
  candidate_width : Double,
) -> Bool {
  match max_width {
    Some(width) => width > 0.0 && candidate_width > width
    None => false
  }
}

///|
fn fallback_paragraph_cluster_is_soft_break(cluster : String) -> Bool {
  cluster == " " ||
  cluster == "\t" ||
  cluster == "-" ||
  cluster == "、" ||
  cluster == "," ||
  cluster == "。" ||
  cluster == ";" ||
  cluster == ":" ||
  cluster == ")" ||
  cluster == ")"
}

///|
fn fallback_paragraph_line_width(
  measured_width : Double,
  input : TextLayoutInput,
) -> Double {
  match input.max_width {
    Some(max_width) => measured_width.min(max_width.max(0.0))
    None => measured_width
  }
}

///|
fn fallback_fill_paragraph_caret_rects(
  caret_rects : Array[Rect],
  range : TextRange,
  measured : TextLayoutResult,
  y~ : Double,
  line_height~ : Double,
) -> Unit {
  let line_length = range.end - range.start
  let line_caret_count = line_length + 1
  for line_offset in 0..= 0 && global < caret_rects.length() {
      let x = if line_offset < measured.caret_positions.length() {
        measured.caret_positions[line_offset]
      } else {
        measured.size.width
      }
      caret_rects[global] = Rect::new(x~, y~, width=1.0, height=line_height)
    }
  }
}

///|
fn fallback_paragraph_visual_order(
  text : String,
) -> TextParagraphVisualOrderMetadata {
  let clusters = TextGraphemeBoundaries::new(text~).cluster_count()
  {
    logical_text: text,
    visual_text: text,
    logical_cluster_count: clusters,
    visual_cluster_count: clusters,
    segment_count: if text == "" {
      0
    } else {
      1
    },
    glyph_count: text.to_array().length(),
    visual_order_differs: false,
    bidi_visual_order_ready: false,
  }
}

///|
fn fallback_text_char_advance(ch : Char, font_size : Double) -> Double {
  if ch == ' ' || ch == '\t' {
    font_size * 0.35
  } else if (ch >= '0' && ch <= '9') ||
    (ch >= 'A' && ch <= 'Z') ||
    (ch >= 'a' && ch <= 'z') {
    font_size * 0.55
  } else {
    font_size * 0.95
  }
}

///|
fn fallback_text_layout_result(input : TextLayoutInput) -> TextLayoutResult {
  let mut width = 0.0
  let carets : Array[Double] = []
  carets.push(0.0)
  let mut cursor = 0.0
  for ch in input.text {
    let advance = fallback_text_char_advance(ch, input.font.size)
    cursor = cursor + advance
    width = width + advance
    carets.push(cursor)
  }
  let carets = fallback_stabilize_grapheme_cluster_carets(input.text, carets)
  {
    size: Size::new(width~, height=input.font.size * 1.25),
    baseline: input.font.size * 0.8,
    caret_positions: carets,
  }
}

///|
test "fallback paragraph derives wrapping from one text measurement" {
  let measure_count = [0]
  let system = TextSystem::new(id="counted-fallback", measure=input => {
    measure_count[0] = measure_count[0] + 1
    fallback_text_layout_result(input)
  })
  let paragraph = system.layout_paragraph(
    TextLayoutInput::new(
      text="one two three four five six seven eight nine ten",
      font=FontSpec::new(size=14.0),
      max_width=80.0,
    ),
  )
  assert_true(paragraph.lines.length() > 1)
  assert_eq(measure_count[0], 1)
  assert_eq(paragraph.caret_rects.length(), 49)
}