// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Minimal `Buffer` port scaffold from `cosmic-text/src/buffer.rs`.
///
/// This MVP focuses on line splitting, cache invalidation and redraw semantics.
pub struct Buffer {
  lines : Array[BufferLine]
  metrics : Metrics
  width_opt : Float?
  height_opt : Float?
  scroll : Scroll
  redraw : Bool
  wrap : Wrap
  monospace_width : Float?
  tab_width : Int
  hinting : Hinting
}

///|
pub fn Buffer::new(metrics : Metrics) -> Buffer {
  Buffer::new_empty(metrics)
}

///|
/// Create an empty Buffer with the provided Metrics.
///
/// Panics (fails) if `metrics.line_height` is zero (aligned with upstream).
pub fn Buffer::new_empty(metrics : Metrics) -> Buffer {
  if metrics.line_height == 0.0F {
    abort("line height cannot be 0")
  }
  Buffer::{
    lines: [],
    metrics,
    width_opt: None,
    height_opt: None,
    scroll: Scroll::default(),
    redraw: false,
    wrap: Wrap::WordOrGlyph,
    monospace_width: None,
    tab_width: 8,
    hinting: Hinting::Disabled,
  }
}

///|
pub fn Buffer::metrics(self : Buffer) -> Metrics {
  self.metrics
}

///|
pub fn Buffer::lines(self : Buffer) -> Array[BufferLine] {
  self.lines
}

///|
pub fn Buffer::redraw(self : Buffer) -> Bool {
  self.redraw
}

///|
pub fn Buffer::set_redraw(self : Buffer, redraw : Bool) -> Buffer {
  if self.redraw == redraw {
    self
  } else {
    Buffer::{ ..self, redraw, }
  }
}

///|
pub fn Buffer::scroll(self : Buffer) -> Scroll {
  self.scroll
}

///|
pub fn Buffer::set_scroll(self : Buffer, scroll : Scroll) -> Buffer {
  if self.scroll.line == scroll.line &&
    self.scroll.vertical == scroll.vertical &&
    self.scroll.horizontal == scroll.horizontal {
    self
  } else {
    Buffer::{ ..self, scroll, redraw: true }
  }
}

///|
pub fn Buffer::wrap(self : Buffer) -> Wrap {
  self.wrap
}

///|
pub fn Buffer::hinting(self : Buffer) -> Hinting {
  self.hinting
}

///|
fn wrap_eq(a : Wrap, b : Wrap) -> Bool {
  match (a, b) {
    (None, None) => true
    (Glyph, Glyph) => true
    (Word, Word) => true
    (WordOrGlyph, WordOrGlyph) => true
    _ => false
  }
}

///|
fn hinting_eq(a : Hinting, b : Hinting) -> Bool {
  match (a, b) {
    (Disabled, Disabled) => true
    (Enabled, Enabled) => true
    _ => false
  }
}

///|
fn float_opt_eq(a : Float?, b : Float?) -> Bool {
  match (a, b) {
    (None, None) => true
    (Some(x), Some(y)) => x == y
    _ => false
  }
}

///|
fn clamp_nonneg_opt(v : Float?) -> Float? {
  match v {
    None => None
    Some(x) => if x < 0.0F { Some(0.0F) } else { Some(x) }
  }
}

///|
fn line_has_tab(s : String) -> Bool {
  for i in 0.. Array[BufferLine] {
  let out : Array[BufferLine] = []
  for line in lines {
    out.push(line.reset_layout())
  }
  out
}

///|
fn reset_shaping_lines_for_tab_width(
  lines : Array[BufferLine],
) -> Array[BufferLine] {
  let out : Array[BufferLine] = []
  for line in lines {
    if line.shape_opt() is Some(_) && line_has_tab(line.text()) {
      out.push(line.reset_shaping())
    } else {
      out.push(line)
    }
  }
  out
}

///|
pub fn Buffer::set_wrap(self : Buffer, wrap : Wrap) -> Buffer {
  if wrap_eq(self.wrap, wrap) {
    self
  } else {
    Buffer::{
      ..self,
      wrap,
      lines: reset_layout_lines(self.lines),
      redraw: true,
    }
  }
}

///|
pub fn Buffer::set_tab_width(self : Buffer, tab_width : Int) -> Buffer {
  if tab_width <= 0 || self.tab_width == tab_width {
    self
  } else {
    Buffer::{
      ..self,
      tab_width,
      lines: reset_shaping_lines_for_tab_width(self.lines),
      redraw: true,
    }
  }
}

///|
pub fn Buffer::set_hinting(self : Buffer, hinting : Hinting) -> Buffer {
  if hinting_eq(self.hinting, hinting) {
    self
  } else {
    Buffer::{
      ..self,
      hinting,
      lines: reset_layout_lines(self.lines),
      redraw: true,
    }
  }
}

///|
pub fn Buffer::set_monospace_width(
  self : Buffer,
  monospace_width : Float?,
) -> Buffer {
  if float_opt_eq(self.monospace_width, monospace_width) {
    self
  } else {
    Buffer::{
      ..self,
      monospace_width,
      lines: reset_layout_lines(self.lines),
      redraw: true,
    }
  }
}

///|
pub fn Buffer::set_metrics(self : Buffer, metrics : Metrics) -> Buffer {
  if metrics.font_size == 0.0F {
    abort("font size cannot be 0")
  }
  if metrics.line_height == 0.0F {
    abort("line height cannot be 0")
  }
  if self.metrics.font_size == metrics.font_size &&
    self.metrics.line_height == metrics.line_height {
    self
  } else {
    Buffer::{
      ..self,
      metrics,
      lines: reset_layout_lines(self.lines),
      redraw: true,
    }
  }
}

///|
pub fn Buffer::set_size(
  self : Buffer,
  width_opt : Float?,
  height_opt : Float?,
) -> Buffer {
  let clamped_width_opt = clamp_nonneg_opt(width_opt)
  let clamped_height_opt = clamp_nonneg_opt(height_opt)
  if float_opt_eq(self.width_opt, clamped_width_opt) &&
    float_opt_eq(self.height_opt, clamped_height_opt) {
    self
  } else {
    Buffer::{
      ..self,
      width_opt: clamped_width_opt,
      height_opt: clamped_height_opt,
      lines: reset_layout_lines(self.lines),
      redraw: true,
    }
  }
}

///|
pub fn Buffer::size(self : Buffer) -> (Float?, Float?) {
  (self.width_opt, self.height_opt)
}

///|
fn slice_string(s : String, start : Int, end : Int) -> String {
  let sb = StringBuilder::new(size_hint=(end - start) * 2)
  sb.write_view(s[:].view(start_offset=start, end_offset=end))
  sb.to_string()
}

///|
/// Set full text of buffer (MVP).
///
/// NOTE: `attrs_list` is copied per line (upstream spans are per line).
pub fn Buffer::set_text(
  self : Buffer,
  text : String,
  attrs_list : AttrsList,
  shaping : Shaping,
) -> Buffer {
  let mut iter = LineIter::new(text)
  let new_lines : Array[BufferLine] = []
  while true {
    let (next_iter, item_opt) = iter.next()
    iter = next_iter
    if item_opt is Some(item) {
      let start = item.0
      let end = item.1
      let ending = item.2
      let line_text = slice_string(text, start, end)
      // MVP: attrs_list is shared; once we implement per-line ranges this will be adjusted.
      new_lines.push(BufferLine::new(line_text, ending, attrs_list, shaping))
    } else {
      break
    }
  }
  // Upstream behavior: ensure there is a trailing line with `LineEnding::None`.
  let need_trailing = if new_lines.length() == 0 {
    true
  } else {
    match new_lines[new_lines.length() - 1].ending() {
      None => false
      _ => true
    }
  }
  if need_trailing {
    new_lines.push(BufferLine::new("", LineEnding::None, attrs_list, shaping))
  }
  Buffer::{ ..self, lines: new_lines, redraw: true }
}

///|
/// Set rich text of buffer using styled spans (pairs of text and attrs).
///
/// This mirrors `cosmic-text`'s `Buffer::set_rich_text` behavior:
/// - Concatenates spans into one string.
/// - Splits into paragraphs using `BidiParagraphs`.
/// - Builds per-line `AttrsList` spans relative to each line.
/// - Uses `LineEnding::default()` for all lines.
pub fn Buffer::set_rich_text(
  self : Buffer,
  spans : Array[(String, Attrs)],
  default_attrs : Attrs,
  shaping : Shaping,
  alignment : Align?,
) -> Buffer {
  let string_sb = StringBuilder::new(size_hint=256)
  let span_data : Array[(Attrs, Int, Int)] = []
  let mut end = 0
  for sp in spans {
    let text = sp.0
    let attrs = sp.1
    let start = end
    end = end + text.length()
    string_sb.write_string(text)
    span_data.push((attrs, start, end))
  }
  let string = string_sb.to_string()

  // Empty text: keep one empty line.
  if string.length() == 0 {
    let line = BufferLine::new(
      "",
      LineEnding::default(),
      AttrsList::new(default_attrs),
      shaping,
    )
    let (line2, _) = line.set_align(alignment)
    return Buffer::{
      ..self,
      lines: [line2],
      scroll: Scroll::default(),
      redraw: true,
    }
  }
  let lines : Array[BufferLine] = []
  let mut paras = BidiParagraphs::new(string)
  while true {
    let start = paras.pos
    match paras.next() {
      None => break
      Some((next, para)) => {
        paras = next
        let end0 = start + para.length()
        let mut attrs_list = AttrsList::new(default_attrs)
        for si in 0.. start { s_start } else { start }
          let isect_end = if s_end < end0 { s_end } else { end0 }
          if isect_start < isect_end && attrs != default_attrs {
            attrs_list = attrs_list.add_span(
              isect_start - start,
              isect_end - start,
              attrs,
            )
          }
        }
        let line = BufferLine::new(
          para,
          LineEnding::default(),
          attrs_list,
          shaping,
        )
        let (line2, _) = line.set_align(alignment)
        lines.push(line2)
      }
    }
  }
  Buffer::{ ..self, lines, scroll: Scroll::default(), redraw: true }
}

///|
/// Shape all lines using a provided FontSystem (best-effort).
///
/// This is a coarse-grained helper for clients that do not want to manage
/// per-line shaping manually yet.
pub fn Buffer::shape_all_with_font_system(
  self : Buffer,
  font_system : FontSystem,
) -> Buffer {
  let lines : Array[BufferLine] = []
  for line in self.lines {
    lines.push(line.shape_with_font_system(font_system, self.tab_width))
  }
  Buffer::{ ..self, lines, }
}

///|
/// Layout all lines using a provided FontSystem (best-effort).
pub fn Buffer::layout_all_with_font_system(
  self : Buffer,
  font_system : FontSystem,
) -> Buffer {
  let lines : Array[BufferLine] = []
  for line in self.lines {
    lines.push(
      line.layout_with_font_system(
        font_system,
        self.metrics.font_size,
        self.width_opt,
        self.wrap,
        self.monospace_width,
        self.tab_width,
        self.hinting,
      ),
    )
  }
  Buffer::{ ..self, lines, }
}

///|
/// Layout all lines using the built-in shaping (no font system).
pub fn Buffer::layout_all(self : Buffer) -> Buffer {
  let lines : Array[BufferLine] = []
  for line in self.lines {
    lines.push(
      line.layout(
        self.metrics.font_size,
        self.width_opt,
        self.wrap,
        self.monospace_width,
        self.tab_width,
        self.hinting,
      ),
    )
  }
  Buffer::{ ..self, lines, }
}

///|
/// A line of visible text for rendering (MVP subset).
pub struct LayoutRun {
  line_i : Int
  line_y : Float
  line_top : Float
  line_height : Float
  text : String
  rtl : Bool
  glyphs : Array[LayoutGlyph]
  line_w : Float
}

///|
pub fn Buffer::layout_runs(self : Buffer) -> Iter[LayoutRun] {
  let runs : Array[LayoutRun] = []
  let n = self.lines.length()
  if n == 0 {
    return runs.iter()
  }
  let start_line = if self.scroll.line < 0 {
    0
  } else if self.scroll.line >= n {
    n
  } else {
    self.scroll.line
  }
  let mut line_top_acc = 0.0F
  for i in start_line.. shape.rtl
      None => return runs.iter()
    }
    let layout_lines = match line.layout_opt() {
      Some(layout) => layout
      None => return runs.iter()
    }
    for l in layout_lines {
      let line_height = if l.line_height_opt is Some(h) {
        h
      } else {
        self.metrics.line_height
      }
      let line_top = line_top_acc - self.scroll.vertical
      let glyph_height = l.max_ascent + l.max_descent
      let centering_offset = (line_height - glyph_height) / 2.0F
      let line_y = line_top + centering_offset + l.max_ascent
      if self.height_opt is Some(h) && line_y - l.max_ascent > h {
        return runs.iter()
      }
      line_top_acc = line_top_acc + line_height
      if line_y + l.max_descent < 0.0F {
        continue
      }
      runs.push(LayoutRun::{
        line_i: i,
        line_y,
        line_top,
        line_height,
        text: line.text(),
        rtl,
        glyphs: l.glyphs,
        line_w: l.w,
      })
    }
  }
  runs.iter()
}

///|
/// Hit-testing: map a physical x/y position to a cursor (MVP).
pub fn Buffer::hit(self : Buffer, x : Float, y : Float) -> Cursor? {
  let buffer = self.layout_all()
  let mut last : Cursor? = None
  for run in buffer.layout_runs() {
    let line_cursor = if run.glyphs.length() == 0 {
      Cursor::new_with_affinity(run.line_i, 0, After)
    } else {
      Cursor::new_with_affinity(
        run.line_i,
        run.glyphs[run.glyphs.length() - 1].end,
        Before,
      )
    }
    last = Some(line_cursor)
    if y >= run.line_top && y < run.line_top + run.line_height {
      if run.glyphs.length() == 0 {
        return Some(Cursor::new_with_affinity(run.line_i, 0, After))
      }
      let glyphs = run.glyphs
      let mut min_start = glyphs[0].start
      let mut max_end = glyphs[0].end
      for g in glyphs {
        if g.start < min_start {
          min_start = g.start
        }
        if g.end > max_end {
          max_end = g.end
        }
      }
      // Clamp to glyph span.
      let last_g = glyphs[glyphs.length() - 1]
      if run.rtl {
        if x <= glyphs[0].x {
          return Some(Cursor::new_with_affinity(run.line_i, max_end, Before))
        }
        if x >= last_g.x + last_g.w {
          return Some(Cursor::new_with_affinity(run.line_i, min_start, After))
        }
      } else {
        if x <= glyphs[0].x {
          return Some(Cursor::new_with_affinity(run.line_i, min_start, After))
        }
        if x >= last_g.x + last_g.w {
          return Some(Cursor::new_with_affinity(run.line_i, max_end, Before))
        }
      }
      for g in glyphs {
        let mid = g.x + g.w / 2.0F
        if x < mid {
          if run.rtl {
            return Some(Cursor::new_with_affinity(run.line_i, g.end, Before))
          }
          return Some(Cursor::new_with_affinity(run.line_i, g.start, After))
        }
        if x < g.x + g.w {
          if run.rtl {
            return Some(Cursor::new_with_affinity(run.line_i, g.start, After))
          }
          return Some(Cursor::new_with_affinity(run.line_i, g.end, Before))
        }
      }
      return Some(line_cursor)
    }
  }
  last
}

///|
/// Cursor motion based on current layout runs (MVP).
///
/// This uses visual lines from `layout_runs()` for vertical movement.
pub fn Buffer::cursor_motion(
  self : Buffer,
  cursor : Cursor,
  cursor_x_opt : Int?,
  motion : Motion,
) -> (Cursor, Int?)? {
  // Layout to get visual line runs.
  let laid_out = self.layout_all()
  let buffer = Buffer::{
    ..laid_out,
    scroll: Scroll::default(),
    height_opt: None,
  }
  let runs : Array[LayoutRun] = []
  for r in buffer.layout_runs() {
    runs.push(r)
  }
  if runs.length() == 0 {
    return None
  }
  fn x_for_index(run : LayoutRun, index : Int) -> Float {
    let glyphs = run.glyphs
    if glyphs.length() == 0 {
      return 0.0F
    }
    // Order-independent mapping (glyphs may be in visual order, not start-index order).
    let mut min_start = glyphs[0].start
    let mut min_start_x0 = glyphs[0].x
    let mut min_start_x1 = glyphs[0].x + glyphs[0].w
    let mut max_end = glyphs[0].end
    let mut max_end_x0 = glyphs[0].x
    let mut max_end_x1 = glyphs[0].x + glyphs[0].w
    for g in glyphs {
      if g.start < min_start {
        min_start = g.start
        min_start_x0 = g.x
        min_start_x1 = g.x + g.w
      }
      if g.end > max_end {
        max_end = g.end
        max_end_x0 = g.x
        max_end_x1 = g.x + g.w
      }
      if index == g.start {
        return if run.rtl { g.x + g.w } else { g.x }
      }
      if index == g.end {
        return if run.rtl { g.x } else { g.x + g.w }
      }
      if index > g.start && index < g.end {
        let span = g.end - g.start
        let off = index - g.start
        let t = Float::from_double(off.to_double() / span.to_double())
        return if run.rtl { g.x + g.w - g.w * t } else { g.x + g.w * t }
      }
    }
    if index <= min_start {
      if run.rtl {
        min_start_x1
      } else {
        min_start_x0
      }
    } else if index >= max_end {
      if run.rtl {
        max_end_x0
      } else {
        max_end_x1
      }
    } else {
      0.0F
    }
  }

  fn index_for_x(run : LayoutRun, x : Float) -> Int {
    if run.glyphs.length() == 0 {
      return 0
    }
    let glyphs = run.glyphs
    // Clamp to the visual extents.
    let mut min_x = glyphs[0].x
    let mut min_x_start = glyphs[0].start
    let mut max_x = glyphs[0].x + glyphs[0].w
    let mut max_x_end = glyphs[0].end
    for g in glyphs {
      if g.x < min_x {
        min_x = g.x
        min_x_start = g.start
      }
      if g.x + g.w > max_x {
        max_x = g.x + g.w
        max_x_end = g.end
      }
    }
    if x <= min_x {
      return if run.rtl { max_x_end } else { min_x_start }
    }
    if x >= max_x {
      return if run.rtl { min_x_start } else { max_x_end }
    }
    for g in glyphs {
      let mid = g.x + g.w / 2.0F
      if x < mid {
        return if run.rtl { g.end } else { g.start }
      }
      if x < g.x + g.w {
        return if run.rtl { g.start } else { g.end }
      }
    }
    if run.rtl {
      min_x_start
    } else {
      max_x_end
    }
  }

  fn int_from_float(v : Float) -> Int {
    v.to_double().to_int()
  }

  fn find_run_idx(cursor : Cursor) -> Int? {
    let mut cur_run_idx : Int? = None
    for i in 0.. max_end {
            max_end = g.end
          }
        }
        if cursor.index >= min_start {
          if cursor.index < max_end {
            cur_run_idx = Some(i)
            break
          }
          if cursor.index == max_end {
            // Prefer the next run if this is a wrap boundary.
            if i + 1 >= runs.length() || runs[i + 1].line_i != cursor.line {
              cur_run_idx = Some(i)
              break
            }
          }
        }
      }
    }
    // If we can't map index -> run (e.g. stale cursor), fall back to first run on the line.
    if cur_run_idx is None {
      for i in 0.. {
      let line_run_idxs : Array[Int] = []
      for i in 0..= line_run_idxs.length() {
        layout = line_run_idxs.length() - 1
      }
      let run = runs[line_run_idxs[layout]]
      if run.glyphs.length() == 0 {
        return Some(
          (Cursor::new_with_affinity(run.line_i, 0, After), cursor_x_opt),
        )
      }
      let mut glyph_i = layout_cursor.glyph
      if glyph_i < 0 {
        glyph_i = 0
      }
      if glyph_i < run.glyphs.length() {
        let glyph = run.glyphs[glyph_i]
        return Some(
          (
            Cursor::new_with_affinity(run.line_i, glyph.start, After),
            cursor_x_opt,
          ),
        )
      }
      let mut max_end = run.glyphs[0].end
      for g in run.glyphs {
        if g.end > max_end {
          max_end = g.end
        }
      }
      return Some(
        (Cursor::new_with_affinity(run.line_i, max_end, Before), cursor_x_opt),
      )
    }
    _ => ()
  }

  // Vertical movement uses visual runs.
  let delta_opt : Int? = match motion {
    Up => Some(-1)
    Down => Some(1)
    Vertical(px) => {
      let lh = int_from_float(buffer.metrics.line_height)
      if lh == 0 {
        None
      } else {
        Some(px / lh)
      }
    }
    PageUp =>
      match buffer.height_opt {
        None => None
        Some(h) => {
          let lh = int_from_float(buffer.metrics.line_height)
          if lh == 0 {
            None
          } else {
            Some(-int_from_float(h) / lh)
          }
        }
      }
    PageDown =>
      match buffer.height_opt {
        None => None
        Some(h) => {
          let lh = int_from_float(buffer.metrics.line_height)
          if lh == 0 {
            None
          } else {
            Some(int_from_float(h) / lh)
          }
        }
      }
    _ => None
  }
  if delta_opt is Some(delta0) {
    if delta0 == 0 {
      return Some((cursor, cursor_x_opt))
    }
    let cur_run_idx_opt = find_run_idx(cursor)
    if cur_run_idx_opt is None {
      return None
    }
    let cur_idx = if cur_run_idx_opt is Some(v) { v } else { 0 }
    let cur_run = runs[cur_idx]
    let cur_x = x_for_index(cur_run, cursor.index)
    let desired_x = match cursor_x_opt {
      None => cur_x
      Some(xi) => Float::from_double(xi.to_double())
    }
    let target = cur_idx + delta0
    if target < 0 || target >= runs.length() {
      return None
    }
    let run = runs[target]
    let new_index = index_for_x(run, desired_x)
    return Some(
      (
        Cursor::new_with_affinity(run.line_i, new_index, cursor.affinity),
        Some(int_from_float(desired_x)),
      ),
    )
  }

  // Fallback: text-index motion (same as Editor's MVP cursor_motion).
  let mut c = cursor
  fn move_prev_grapheme(buffer : Buffer, c0 : Cursor) -> Cursor {
    let mut c = c0
    if c.index > 0 {
      let text = buffer.lines()[c.line].text()
      let ranges = grapheme_indices_uax29(text)
      let mut found = 0
      for r in ranges {
        if c.index > r.0 && c.index <= r.1 {
          found = r.0
        }
      }
      c = Cursor::new_with_affinity(c.line, found, Affinity::After)
    } else if c.line > 0 {
      let prev = c.line - 1
      let prev_len = buffer.lines()[prev].text().length()
      c = Cursor::new_with_affinity(prev, prev_len, Affinity::After)
    }
    c
  }

  fn move_next_grapheme(buffer : Buffer, c0 : Cursor) -> Cursor {
    let mut c = c0
    let len = buffer.lines()[c.line].text().length()
    if c.index < len {
      let text = buffer.lines()[c.line].text()
      let ranges = grapheme_indices_uax29(text)
      let mut found = len
      for r in ranges {
        if c.index >= r.0 && c.index < r.1 {
          found = r.1
          break
        }
      }
      c = Cursor::new_with_affinity(c.line, found, Affinity::Before)
    } else if c.line + 1 < buffer.lines().length() {
      c = Cursor::new_with_affinity(c.line + 1, 0, Affinity::Before)
    }
    c
  }

  fn move_prev_word(buffer : Buffer, c0 : Cursor) -> Cursor {
    let mut c = c0
    let text = buffer.lines()[c.line].text()
    if c.index > 0 {
      let ranges = word_indices_uax29(text)
      let mut found = 0
      let mut i = ranges.length() - 1
      while i >= 0 {
        let (ws, _we) = ranges[i]
        if ws < c.index {
          found = ws
          break
        }
        if i == 0 {
          break
        }
        i = i - 1
      }
      c = Cursor::new_with_affinity(c.line, found, c.affinity)
    } else if c.line > 0 {
      let prev = c.line - 1
      c = Cursor::new_with_affinity(
        prev,
        buffer.lines()[prev].text().length(),
        c.affinity,
      )
    }
    c
  }

  fn move_next_word(buffer : Buffer, c0 : Cursor) -> Cursor {
    let mut c = c0
    let text = buffer.lines()[c.line].text()
    if c.index < text.length() {
      let ranges = word_indices_uax29(text)
      let mut found = text.length()
      for pair in ranges {
        let we = pair.1
        if we > c.index {
          found = we
          break
        }
      }
      c = Cursor::new_with_affinity(c.line, found, c.affinity)
    } else if c.line + 1 < buffer.lines().length() {
      c = Cursor::new_with_affinity(c.line + 1, 0, c.affinity)
    }
    c
  }

  match motion {
    Previous => c = move_prev_grapheme(buffer, c)
    Next => c = move_next_grapheme(buffer, c)
    Left => {
      let rtl = match buffer.lines()[c.line].shape_opt() {
        None => false
        Some(s) => s.rtl
      }
      c = if rtl {
        move_next_grapheme(buffer, c)
      } else {
        move_prev_grapheme(buffer, c)
      }
    }
    Right => {
      let rtl = match buffer.lines()[c.line].shape_opt() {
        None => false
        Some(s) => s.rtl
      }
      c = if rtl {
        move_prev_grapheme(buffer, c)
      } else {
        move_next_grapheme(buffer, c)
      }
    }
    PreviousWord => c = move_prev_word(buffer, c)
    NextWord => c = move_next_word(buffer, c)
    LeftWord => {
      let rtl = match buffer.lines()[c.line].shape_opt() {
        None => false
        Some(s) => s.rtl
      }
      c = if rtl {
        move_next_word(buffer, c)
      } else {
        move_prev_word(buffer, c)
      }
    }
    RightWord => {
      let rtl = match buffer.lines()[c.line].shape_opt() {
        None => false
        Some(s) => s.rtl
      }
      c = if rtl {
        move_prev_word(buffer, c)
      } else {
        move_next_word(buffer, c)
      }
    }
    Home =>
      match find_run_idx(c) {
        None => c = Cursor::new_with_affinity(c.line, 0, After)
        Some(idx) => {
          let run = runs[idx]
          if run.glyphs.length() == 0 {
            c = Cursor::new_with_affinity(c.line, 0, After)
          } else {
            let mut min_start = run.glyphs[0].start
            for g in run.glyphs {
              if g.start < min_start {
                min_start = g.start
              }
            }
            c = Cursor::new_with_affinity(c.line, min_start, After)
          }
        }
      }
    End =>
      match find_run_idx(c) {
        None =>
          c = Cursor::new_with_affinity(
            c.line,
            buffer.lines()[c.line].text().length(),
            Before,
          )
        Some(idx) => {
          let run = runs[idx]
          if run.glyphs.length() == 0 {
            c = Cursor::new_with_affinity(c.line, 0, Before)
          } else {
            let mut max_end = run.glyphs[0].end
            for g in run.glyphs {
              if g.end > max_end {
                max_end = g.end
              }
            }
            c = Cursor::new_with_affinity(c.line, max_end, Before)
          }
        }
      }
    SoftHome => {
      let text = buffer.lines()[c.line].text()
      // Upstream `cosmic-text` behavior: always jump to the first non-whitespace
      // character on the line, or 0 if the line is all whitespace.
      let mut target = 0
      for p in text.iter2() {
        let idx = p.0
        let ch = p.1
        if !ch.is_whitespace() {
          target = idx
          break
        }
      }
      c = Cursor::new_with_affinity(c.line, target, c.affinity)
    }
    BufferStart => c = Cursor::new_with_affinity(0, 0, c.affinity)
    BufferEnd => {
      let last = buffer.lines().length() - 1
      c = Cursor::new_with_affinity(
        last,
        buffer.lines()[last].text().length(),
        c.affinity,
      )
    }
    ParagraphStart => c = Cursor::new_with_affinity(c.line, 0, c.affinity)
    ParagraphEnd =>
      c = Cursor::new_with_affinity(
        c.line,
        buffer.lines()[c.line].text().length(),
        c.affinity,
      )
    GotoLine(line_i) => {
      if line_i < 0 || line_i >= buffer.lines().length() {
        return None
      }
      match find_run_idx(c) {
        None => c = Cursor::new_with_affinity(line_i, 0, c.affinity)
        Some(run_idx) => {
          // Find current line-local visual layout and glyph indices.
          let mut layout = 0
          for i in 0.. 0 {
            let mut found = false
            for gi in 0.. max_end {
                  max_end = g.end
                }
              }
              if c.index == max_end {
                glyph = run.glyphs.length()
              }
            }
          }
          let lc = LayoutCursor::new(line_i, layout, glyph)
          match
            buffer.cursor_motion(c, cursor_x_opt, Motion::LayoutCursor(lc)) {
            None => return None
            Some((nc, nx)) => return Some((nc, nx))
          }
        }
      }
    }
    _ => ()
  }
  Some((c, None))
}

///|
/// Draw the buffer using a `SwashCache`.
///
/// For compatibility with upstream's legacy renderer helper, the callback is
/// called per pixel (`w = 1`, `h = 1`).
pub fn Buffer::draw_with_font_system(
  self : Buffer,
  font_system : FontSystem,
  swash_cache : SwashCache,
  text_color : Color,
  f : (Int, Int, UInt, UInt, Color) -> Unit,
) -> (Buffer, SwashCache) {
  let buffer = self.layout_all_with_font_system(font_system)
  for run in buffer.layout_runs() {
    for glyph in run.glyphs {
      let physical = glyph.physical((0.0F, run.line_y), 1.0F)
      swash_cache.with_pixels(font_system, physical.cache_key, text_color, fn(
        px,
        py,
        color,
      ) {
        f(physical.x + px, physical.y + py, 1U, 1U, color)
      })
    }
  }
  (buffer, swash_cache)
}

///|
/// Draw the buffer with a font system and swash cache.
///
/// This is a naming-aligned convenience wrapper (matches upstream's `Buffer::draw` intent).
pub fn Buffer::draw(
  self : Buffer,
  font_system : FontSystem,
  swash_cache : SwashCache,
  text_color : Color,
  f : (Int, Int, UInt, UInt, Color) -> Unit,
) -> (Buffer, SwashCache) {
  self.draw_with_font_system(font_system, swash_cache, text_color, f)
}