// 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`.
pub struct Buffer {
  lines : Array[BufferLine]
  metrics : Metrics
  width_opt : Float?
  height_opt : Float?
  scroll : Scroll
  redraw : Bool
  wrap : Wrap
  ellipsize : Ellipsize
  monospace_width : Float?
  tab_width : Int
  hinting : Hinting
  dirty_relayout : Bool
  dirty_tab_shape : Bool
  dirty_text_set : Bool
  dirty_scroll : Bool
}

///|
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,
    ellipsize: Ellipsize::None,
    monospace_width: None,
    tab_width: 8,
    hinting: Hinting::Disabled,
    dirty_relayout: false,
    dirty_tab_shape: false,
    dirty_text_set: false,
    dirty_scroll: false,
  }
}

///|
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, dirty_scroll: true }
  }
}

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

///|
pub fn Buffer::ellipsize(self : Buffer) -> Ellipsize {
  self.ellipsize
}

///|
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 ellipsize_height_limit_eq(
  a : EllipsizeHeightLimit,
  b : EllipsizeHeightLimit,
) -> Bool {
  match (a, b) {
    (Lines(a0), Lines(b0)) => a0 == b0
    (Height(a0), Height(b0)) => a0 == b0
    _ => false
  }
}

///|
fn ellipsize_eq(a : Ellipsize, b : Ellipsize) -> Bool {
  match (a, b) {
    (None, None) => true
    (Start(a0), Start(b0)) => ellipsize_height_limit_eq(a0, b0)
    (Middle(a0), Middle(b0)) => ellipsize_height_limit_eq(a0, b0)
    (End(a0), End(b0)) => ellipsize_height_limit_eq(a0, b0)
    _ => 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 layout_total_height(
  metrics : Metrics,
  layout_opt : Array[LayoutLine]?,
) -> Float {
  match layout_opt {
    None => metrics.line_height
    Some(layout) => {
      let mut h = 0.0F
      for layout_line in layout {
        let line_height = if layout_line.line_height_opt is Some(v) {
          v
        } else {
          metrics.line_height
        }
        h = h + line_height
      }
      h
    }
  }
}

///|
fn ensure_line_layout_cached(
  lines : Array[BufferLine],
  line_i : Int,
  font_system : FontSystem,
  font_size : Float,
  width_opt : Float?,
  wrap : Wrap,
  ellipsize : Ellipsize,
  monospace_width : Float?,
  tab_width : Int,
  hinting : Hinting,
) -> Array[LayoutLine]? {
  if line_i < 0 || line_i >= lines.length() {
    return None
  }
  let line = lines[line_i].layout_with_font_system(
    font_system, font_size, width_opt, wrap, ellipsize, monospace_width, tab_width,
    hinting,
  )
  lines.set(line_i, line)
  line.layout_opt()
}

///|
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
}

///|
fn buffer_has_dirty(buffer : Buffer) -> Bool {
  buffer.dirty_relayout ||
  buffer.dirty_tab_shape ||
  buffer.dirty_text_set ||
  buffer.dirty_scroll
}

///|
fn buffer_resolve_dirty(buffer : Buffer) -> Buffer? {
  if !buffer_has_dirty(buffer) {
    return None
  }
  let lines = buffer.lines
  if !buffer.dirty_text_set {
    let lines = if buffer.dirty_tab_shape {
      reset_shaping_lines_for_tab_width(lines)
    } else {
      lines
    }
    let lines = if buffer.dirty_relayout {
      reset_layout_lines(lines)
    } else {
      lines
    }
    return Some(Buffer::{
      ..buffer,
      lines,
      redraw: true,
      dirty_relayout: false,
      dirty_tab_shape: false,
      dirty_text_set: false,
      dirty_scroll: false,
    })
  }
  Some(Buffer::{
    ..buffer,
    lines,
    redraw: true,
    dirty_relayout: false,
    dirty_tab_shape: false,
    dirty_text_set: false,
    dirty_scroll: false,
  })
}

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

///|
pub fn Buffer::set_ellipsize(self : Buffer, ellipsize : Ellipsize) -> Buffer {
  if ellipsize_eq(self.ellipsize, ellipsize) {
    self
  } else {
    Buffer::{ ..self, ellipsize, redraw: true, dirty_relayout: 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,
      redraw: true,
      dirty_tab_shape: true,
      dirty_relayout: true,
    }
  }
}

///|
pub fn Buffer::set_hinting(self : Buffer, hinting : Hinting) -> Buffer {
  if hinting_eq(self.hinting, hinting) {
    self
  } else {
    Buffer::{ ..self, hinting, redraw: true, dirty_relayout: 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, redraw: true, dirty_relayout: true }
  }
}

///|
pub fn Buffer::set_metrics(self : Buffer, metrics : Metrics) -> Buffer {
  self.set_metrics_and_size(metrics, self.width_opt, self.height_opt)
}

///|
pub fn Buffer::set_metrics_and_size(
  self : Buffer,
  metrics : Metrics,
  width_opt : Float?,
  height_opt : Float?,
) -> 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")
  }
  let clamped_width_opt = clamp_nonneg_opt(width_opt)
  let clamped_height_opt = clamp_nonneg_opt(height_opt)
  if self.metrics.font_size == metrics.font_size &&
    self.metrics.line_height == metrics.line_height &&
    float_opt_eq(self.width_opt, clamped_width_opt) &&
    float_opt_eq(self.height_opt, clamped_height_opt) {
    return self
  }
  Buffer::{
    ..self,
    metrics,
    width_opt: clamped_width_opt,
    height_opt: clamped_height_opt,
    redraw: true,
    dirty_relayout: true,
  }
}

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

///|
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.
///
/// 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 = self.lines
  let mut line_count = 0
  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)
      if line_count < new_lines.length() {
        let line = new_lines[line_count]
        let (next_line0, _) = line.set_text(line_text, ending, attrs_list)
        let (next_line, _) = next_line0.set_align(None)
        new_lines.set(line_count, next_line)
      } else {
        // `attrs_list` is currently shared across split lines.
        new_lines.push(BufferLine::new(line_text, ending, attrs_list, shaping))
      }
      line_count = line_count + 1
    } else {
      break
    }
  }
  // Upstream behavior: ensure there is a trailing line with `LineEnding::None`.
  let need_trailing = if line_count == 0 {
    true
  } else {
    match new_lines[line_count - 1].ending() {
      None => false
      _ => true
    }
  }
  if need_trailing {
    if line_count < new_lines.length() {
      let line = new_lines[line_count]
      let (next_line0, _) = line.set_text("", LineEnding::None, attrs_list)
      let (next_line, _) = next_line0.set_align(None)
      new_lines.set(line_count, next_line)
    } else {
      new_lines.push(BufferLine::new("", LineEnding::None, attrs_list, shaping))
    }
    line_count = line_count + 1
  }
  new_lines.truncate(line_count)
  Buffer::{
    ..self,
    lines: new_lines,
    scroll: Scroll::default(),
    redraw: true,
    dirty_relayout: false,
    dirty_tab_shape: false,
    dirty_text_set: true,
    dirty_scroll: false,
  }
}

///|
/// 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,
      dirty_relayout: false,
      dirty_tab_shape: false,
      dirty_text_set: true,
      dirty_scroll: false,
    }
  }
  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,
    dirty_relayout: false,
    dirty_tab_shape: false,
    dirty_text_set: true,
    dirty_scroll: false,
  }
}

///|
/// 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 resolved = match buffer_resolve_dirty(self) {
    None => self
    Some(v) => v
  }
  let lines : Array[BufferLine] = []
  for line in resolved.lines {
    lines.push(line.shape_with_font_system(font_system, resolved.tab_width))
  }
  Buffer::{ ..resolved, 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 resolved = match buffer_resolve_dirty(self) {
    None => self
    Some(v) => v
  }
  let lines : Array[BufferLine] = []
  for line in resolved.lines {
    lines.push(
      line.layout_with_font_system(
        font_system,
        resolved.metrics.font_size,
        resolved.width_opt,
        resolved.wrap,
        resolved.ellipsize,
        resolved.monospace_width,
        resolved.tab_width,
        resolved.hinting,
      ),
    )
  }
  Buffer::{ ..resolved, lines, }
}

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

///|
pub fn Buffer::line_shape(
  self : Buffer,
  font_system : FontSystem,
  line_i : Int,
) -> ShapeLine? {
  if line_i < 0 || line_i >= self.lines.length() {
    return None
  }
  self.lines[line_i]
  .shape_with_font_system(font_system, self.tab_width)
  .shape_opt()
}

///|
pub fn Buffer::line_layout(
  self : Buffer,
  font_system : FontSystem,
  line_i : Int,
) -> Array[LayoutLine]? {
  if line_i < 0 || line_i >= self.lines.length() {
    return None
  }
  self.lines[line_i]
  .layout_with_font_system(
    font_system,
    self.metrics.font_size,
    self.width_opt,
    self.wrap,
    self.ellipsize,
    self.monospace_width,
    self.tab_width,
    self.hinting,
  )
  .layout_opt()
}

///|
pub fn Buffer::shape_until_scroll(
  self : Buffer,
  font_system : FontSystem,
  prune : Bool,
) -> Buffer {
  let resolved = match buffer_resolve_dirty(self) {
    None => return self
    Some(v) => v
  }
  let metrics = resolved.metrics
  let old_scroll = resolved.scroll
  let mut scroll = resolved.scroll
  let lines = resolved.lines

  while true {
    // Keep vertical offset non-negative by moving to previous lines.
    while scroll.vertical < 0.0F {
      if scroll.line > 0 {
        let line_i = scroll.line - 1
        let layout_opt = ensure_line_layout_cached(
          lines,
          line_i,
          font_system,
          resolved.metrics.font_size,
          resolved.width_opt,
          resolved.wrap,
          resolved.ellipsize,
          resolved.monospace_width,
          resolved.tab_width,
          resolved.hinting,
        )
        let layout_height = layout_total_height(metrics, layout_opt)
        scroll = Scroll::{
          ..scroll,
          line: line_i,
          vertical: scroll.vertical + layout_height,
        }
      } else {
        scroll = Scroll::{ ..scroll, vertical: 0.0F }
        break
      }
    }

    let scroll_start = scroll.vertical
    let height_limit = if self.height_opt is Some(h) { h } else { 1.0e30F }
    let scroll_end = scroll_start + height_limit

    let mut total_height = 0.0F
    let mut line_i = 0
    while line_i < lines.length() {
      if line_i < scroll.line {
        if prune {
          lines.set(line_i, lines[line_i].reset_shaping())
        }
        line_i = line_i + 1
        continue
      }
      if total_height > scroll_end {
        if prune {
          lines.set(line_i, lines[line_i].reset_shaping())
          line_i = line_i + 1
          continue
        }
        break
      }

      let layout_opt = ensure_line_layout_cached(
        lines,
        line_i,
        font_system,
        resolved.metrics.font_size,
        resolved.width_opt,
        resolved.wrap,
        resolved.ellipsize,
        resolved.monospace_width,
        resolved.tab_width,
        resolved.hinting,
      )
      let layout_height = layout_total_height(metrics, layout_opt)
      total_height = total_height + layout_height
      if line_i == scroll.line && layout_height <= scroll.vertical {
        scroll = Scroll::{
          ..scroll,
          line: scroll.line + 1,
          vertical: scroll.vertical - layout_height,
        }
      }
      line_i = line_i + 1
    }

    if total_height < scroll_end && scroll.line > 0 {
      scroll = Scroll::{
        ..scroll,
        vertical: scroll.vertical - (scroll_end - total_height),
      }
    } else {
      break
    }
  }

  let redraw = resolved.redraw ||
    old_scroll.line != scroll.line ||
    old_scroll.vertical != scroll.vertical ||
    old_scroll.horizontal != scroll.horizontal
  Buffer::{ ..resolved, lines, scroll, redraw }
}

///|
pub fn Buffer::layout_cursor(
  self : Buffer,
  font_system : FontSystem,
  cursor : Cursor,
) -> LayoutCursor? {
  let layout = self.line_layout(font_system, cursor.line)
  if layout is None {
    return None
  }
  let lines = if layout is Some(v) { v } else { [] }
  for p in lines.iter2() {
    let layout_i = p.0
    let layout_line = p.1
    for gp in layout_line.glyphs.iter2() {
      let glyph_i = gp.0
      let glyph = gp.1
      let cursor_end = Cursor::new_with_affinity(cursor.line, glyph.end, Before)
      let cursor_start = Cursor::new_with_affinity(
        cursor.line,
        glyph.start,
        After,
      )
      let glyph_ltr = glyph.level % 2 == 0
      let cursor_left = if glyph_ltr { cursor_start } else { cursor_end }
      let cursor_right = if glyph_ltr { cursor_end } else { cursor_start }
      if cursor == cursor_left {
        return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i))
      }
      if cursor == cursor_right {
        return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i + 1))
      }
    }
  }
  Some(LayoutCursor::new(cursor.line, 0, 0))
}

///|
pub fn Buffer::shape_until_cursor(
  self : Buffer,
  font_system : FontSystem,
  cursor : Cursor,
  prune : Bool,
) -> Buffer {
  let base = self.shape_until_scroll(font_system, prune)
  if cursor.line < 0 || cursor.line >= base.lines.length() {
    return base
  }

  let metrics = base.metrics
  let old_scroll = base.scroll

  let layout_cursor = match base.layout_cursor(font_system, cursor) {
    Some(v) => v
    None => return base
  }
  let layout = match base.line_layout(font_system, layout_cursor.line) {
    Some(v) => v
    None => return base
  }
  if layout_cursor.layout < 0 || layout_cursor.layout >= layout.length() {
    return base
  }

  let mut layout_y = 0.0F
  for i in 0.. layout_cursor.line ||
    (scroll.line == layout_cursor.line && scroll.vertical > layout_y) {
    scroll = Scroll::{ ..scroll, line: layout_cursor.line, vertical: layout_y }
  } else if base.height_opt is Some(height) {
    let mut line_i = layout_cursor.line
    if line_i <= scroll.line {
      if total_height > height + scroll.vertical {
        scroll = Scroll::{ ..scroll, vertical: total_height - height }
      }
    } else {
      while line_i > scroll.line {
        line_i = line_i - 1
        match base.line_layout(font_system, line_i) {
          Some(line_layout) =>
            for layout_line in line_layout {
              let line_height = if layout_line.line_height_opt is Some(v) {
                v
              } else {
                metrics.line_height
              }
              total_height = total_height + line_height
            }
          None => total_height = total_height + metrics.line_height
        }
        if total_height > height + scroll.vertical {
          scroll = Scroll::{
            ..scroll,
            line: line_i,
            vertical: total_height - height,
          }
        }
      }
    }
  }

  let mut buffer = if scroll.line != base.scroll.line ||
    scroll.vertical != base.scroll.vertical ||
    scroll.horizontal != base.scroll.horizontal {
    Buffer::{ ..base, scroll, redraw: true, dirty_scroll: true }
  } else {
    base
  }

  buffer = buffer.shape_until_scroll(font_system, prune)

  match buffer.layout_cursor(font_system, cursor) {
    Some(layout_cursor2) =>
      match buffer.line_layout(font_system, layout_cursor2.line) {
        Some(layout_lines) =>
          match layout_lines.get(layout_cursor2.layout) {
            Some(layout_line) => {
              let glyph_opt = match
                layout_line.glyphs.get(layout_cursor2.glyph) {
                Some(v) => Some(v)
                None =>
                  if layout_line.glyphs.length() > 0 {
                    Some(layout_line.glyphs[layout_line.glyphs.length() - 1])
                  } else {
                    None
                  }
              }
              match glyph_opt {
                Some(glyph) => {
                  let x_a = glyph.x
                  let x_b = glyph.x + glyph.w
                  let x_min = if x_a < x_b { x_a } else { x_b }
                  let x_max = if x_a > x_b { x_a } else { x_b }
                  if x_min < buffer.scroll.horizontal {
                    buffer = Buffer::{
                      ..buffer,
                      scroll: Scroll::{ ..buffer.scroll, horizontal: x_min },
                      redraw: true,
                    }
                  }
                  if buffer.width_opt is Some(width) &&
                    x_max > buffer.scroll.horizontal + width {
                    buffer = Buffer::{
                      ..buffer,
                      scroll: Scroll::{
                        ..buffer.scroll,
                        horizontal: x_max - width,
                      },
                      redraw: true,
                    }
                  }
                }
                None => ()
              }
            }
            None => ()
          }
        None => ()
      }
    None => ()
  }

  if old_scroll.line != buffer.scroll.line ||
    old_scroll.vertical != buffer.scroll.vertical ||
    old_scroll.horizontal != buffer.scroll.horizontal {
    Buffer::{ ..buffer, redraw: true }
  } else {
    buffer
  }
}

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

///|
fn affinity_rank(a : Affinity) -> Int {
  match a {
    Before => 0
    After => 1
  }
}

///|
fn cursor_compare(a : Cursor, b : Cursor) -> Int {
  if a.line != b.line {
    if a.line < b.line {
      -1
    } else {
      1
    }
  } else if a.index != b.index {
    if a.index < b.index {
      -1
    } else {
      1
    }
  } else {
    let ar = affinity_rank(a.affinity)
    let br = affinity_rank(b.affinity)
    if ar < br {
      -1
    } else if ar > br {
      1
    } else {
      0
    }
  }
}

///|
fn cursor_in_range(cursor : Cursor, start : Cursor, end : Cursor) -> Bool {
  cursor_compare(cursor, start) >= 0 && cursor_compare(cursor, end) <= 0
}

///|
fn run_cursor_from_glyph_left(run : LayoutRun, glyph : LayoutGlyph) -> Cursor {
  if run.rtl {
    Cursor::new_with_affinity(run.line_i, glyph.end, Before)
  } else {
    Cursor::new_with_affinity(run.line_i, glyph.start, After)
  }
}

///|
fn run_cursor_from_glyph_right(run : LayoutRun, glyph : LayoutGlyph) -> Cursor {
  if run.rtl {
    Cursor::new_with_affinity(run.line_i, glyph.start, After)
  } else {
    Cursor::new_with_affinity(run.line_i, glyph.end, Before)
  }
}

///|
/// Return highlighted x-span `(x_left, width)` intersecting this run.
pub fn LayoutRun::highlight(
  self : LayoutRun,
  cursor_start : Cursor,
  cursor_end : Cursor,
) -> (Float, Float)? {
  let mut x_start : Float? = None
  let mut x_end : Float? = None
  let rtl_factor = if self.rtl { 1.0F } else { 0.0F }
  let ltr_factor = 1.0F - rtl_factor
  for glyph in self.glyphs {
    let cursor_left = run_cursor_from_glyph_left(self, glyph)
    if cursor_in_range(cursor_left, cursor_start, cursor_end) {
      let x = glyph.x + glyph.w * rtl_factor
      if x_start is None {
        x_start = Some(x)
      }
      x_end = Some(x)
    }
    let cursor_right = run_cursor_from_glyph_right(self, glyph)
    if cursor_in_range(cursor_right, cursor_start, cursor_end) {
      let x = glyph.x + glyph.w * ltr_factor
      if x_start is None {
        x_start = Some(x)
      }
      x_end = Some(x)
    }
  }
  match (x_start, x_end) {
    (Some(a), Some(b)) => {
      let min_x = if a < b { a } else { b }
      let max_x = if a > b { a } else { b }
      Some((min_x, max_x - min_x))
    }
    _ => None
  }
}

///|
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,
        decorations: l.decorations,
        line_w: l.w,
      })
    }
  }
  runs.iter()
}

///|
/// Hit-testing: map a physical x/y position to a cursor.
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.
///
/// 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 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)
  let renderer = LegacyRenderer::new(font_system, swash_cache, f)
  for run in buffer.layout_runs() {
    for glyph in run.glyphs {
      let physical = glyph.physical((0.0F, run.line_y), 1.0F)
      let color = if glyph.color_opt is Some(c) { c } else { text_color }
      renderer.glyph(physical, color)
    }
    render_decoration(renderer, run, text_color)
  }
  (buffer, renderer.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)
}