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

///|
/// Port scaffold from `cosmic-text/src/buffer_line.rs`.
///
/// This implementation is intentionally minimal and test-driven:
/// - It focuses on cache invalidation and text/span mutations (append/split_off),
///   keeping API naming/semantics aligned with upstream.
fn string_concat(a : String, b : String) -> String {
  let sb = StringBuilder::new(size_hint=(a.length() + b.length()) * 2)
  sb.write_string(a)
  sb.write_string(b)
  sb.to_string()
}

///|
fn substring(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()
}

///|
fn empty_layout_line() -> LayoutLine {
  LayoutLine::{
    start: 0,
    end: 0,
    w: 0.0F,
    max_ascent: 0.0F,
    max_descent: 0.0F,
    line_height_opt: None,
    glyphs: [],
    decorations: [],
  }
}

///|
fn max_layout_width(layout : Array[LayoutLine]) -> Float? {
  if layout.length() == 0 {
    return None
  }
  let mut max_w = layout[0].w
  for l in layout {
    if l.w > max_w {
      max_w = l.w
    }
  }
  Some(max_w)
}

///|
fn justify_line(text : String, line : LayoutLine, width : Float) -> LayoutLine {
  let extra = width - line.w
  if extra <= 0.0F {
    return line
  }
  let mut space_count = 0
  for i in line.start..= 0 && i < text.length() && text.code_unit_at(i) == 32 {
      space_count = space_count + 1
    }
  }
  if space_count == 0 {
    return line
  }
  let add = extra / Float::from_double(space_count.to_double())
  let glyphs : Array[LayoutGlyph] = []
  let mut dx = 0.0F
  for g in line.glyphs {
    let mut w = g.w
    if g.end - g.start == 1 &&
      g.start >= 0 &&
      g.start < text.length() &&
      text.code_unit_at(g.start) == 32 {
      w = w + add
      glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx, w })
      dx = dx + add
    } else {
      glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx, w })
    }
  }
  LayoutLine::{ ..line, w: line.w + extra, glyphs }
}

///|
fn justify_lines_except_last(
  text : String,
  layout : Array[LayoutLine],
  width : Float,
) -> Array[LayoutLine] {
  let out : Array[LayoutLine] = []
  let n = layout.length()
  for i in 0.. LayoutLine {
  if l.glyphs.length() == 0 {
    return l
  }
  let glyphs : Array[LayoutGlyph] = []
  let mut x = roundf(l.glyphs[0].x)
  let start_x = x
  for g in l.glyphs {
    let w = roundf(g.w)
    glyphs.push(LayoutGlyph::{ ..g, x, w })
    x = x + w
  }
  LayoutLine::{ ..l, w: x - start_x, glyphs }
}

///|
fn nonneg(v : Float) -> Float {
  if v < 0.0F {
    0.0F
  } else {
    v
  }
}

///|
fn bl_maxf(a : Float, b : Float) -> Float {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn layout_line_height_for_ellipsize(
  line : LayoutLine,
  font_size : Float,
) -> Float {
  if line.line_height_opt is Some(h) {
    h
  } else {
    font_size
  }
}

///|
fn ellipsis_width(line : LayoutLine, font_size : Float) -> Float {
  if line.glyphs.length() == 0 {
    return font_size
  }
  let g = line.glyphs[0]
  let w = if g.w > 0.0F { g.w } else { g.font_size }
  if w > 0.0F {
    w
  } else {
    font_size
  }
}

///|
fn ellipsis_glyph(
  line : LayoutLine,
  font_size : Float,
  x : Float,
  w : Float,
) -> LayoutGlyph {
  if line.glyphs.length() > 0 {
    let base = line.glyphs[0]
    LayoutGlyph::{ ..base, start: line.end, end: line.end, x, w }
  } else {
    LayoutGlyph::{
      ..LayoutGlyph::new(
        line.start,
        line.end,
        font_size,
        0,
        400,
        0,
        x,
        0.0F,
        w,
        0,
      ),
      line_height_opt: line.line_height_opt,
    }
  }
}

///|
fn take_prefix_indices(
  glyphs : Array[LayoutGlyph],
  max_w : Float,
) -> Array[Int] {
  let out : Array[Int] = []
  let mut w = 0.0F
  for p in glyphs.iter2() {
    let i = p.0
    let g = p.1
    if w + g.w > max_w {
      break
    }
    out.push(i)
    w = w + g.w
  }
  out
}

///|
fn take_suffix_indices(
  glyphs : Array[LayoutGlyph],
  max_w : Float,
) -> Array[Int] {
  let rev : Array[Int] = []
  let mut w = 0.0F
  let mut i = glyphs.length()
  while i > 0 {
    let g = glyphs[i - 1]
    if w + g.w > max_w {
      break
    }
    rev.push(i - 1)
    w = w + g.w
    i = i - 1
  }
  let out : Array[Int] = []
  let mut j = rev.length()
  while j > 0 {
    out.push(rev[j - 1])
    j = j - 1
  }
  out
}

///|
fn glyphs_from_indices(
  glyphs : Array[LayoutGlyph],
  indices : Array[Int],
) -> Array[LayoutGlyph] {
  let out : Array[LayoutGlyph] = []
  for idx in indices {
    if glyphs.get(idx) is Some(g) {
      out.push(g)
    }
  }
  out
}

///|
fn reposition_glyphs(
  glyphs : Array[LayoutGlyph],
  start_x : Float,
) -> (Array[LayoutGlyph], Float) {
  let out : Array[LayoutGlyph] = []
  let mut x = start_x
  for g in glyphs {
    out.push(LayoutGlyph::{ ..g, x, })
    x = x + g.w
  }
  (out, x - start_x)
}

///|
fn project_decorations(
  decorations : Array[DecorationSpan],
  source_indices : Array[Int],
) -> Array[DecorationSpan] {
  let out : Array[DecorationSpan] = []
  for decoration in decorations {
    let mut run_start = -1
    let mut i = 0
    while i < source_indices.length() {
      let source_i = source_indices[i]
      let in_span = source_i >= decoration.glyph_start &&
        source_i < decoration.glyph_end
      if in_span {
        if run_start < 0 {
          run_start = i
        }
      } else if run_start >= 0 {
        out.push(DecorationSpan::{
          ..decoration,
          glyph_start: run_start,
          glyph_end: i,
        })
        run_start = -1
      }
      i = i + 1
    }
    if run_start >= 0 {
      out.push(DecorationSpan::{
        ..decoration,
        glyph_start: run_start,
        glyph_end: source_indices.length(),
      })
    }
  }
  out
}

///|
fn ellipsize_line(
  line : LayoutLine,
  font_size : Float,
  width_limit : Float,
  mode : Ellipsize,
) -> LayoutLine {
  let max_w = nonneg(width_limit)
  let e_w = bl_maxf(1.0F, ellipsis_width(line, font_size))
  let text_w = nonneg(max_w - e_w)
  let glyphs = line.glyphs
  let out_glyphs : Array[LayoutGlyph] = []
  let source_indices : Array[Int] = []

  match mode {
    End(_) => {
      let prefix_indices = take_prefix_indices(glyphs, text_w)
      let prefix = glyphs_from_indices(glyphs, prefix_indices)
      let (placed_prefix, prefix_w) = reposition_glyphs(prefix, 0.0F)
      for g in placed_prefix {
        out_glyphs.push(g)
      }
      for idx in prefix_indices {
        source_indices.push(idx)
      }
      out_glyphs.push(ellipsis_glyph(line, font_size, prefix_w, e_w))
      source_indices.push(-1)
    }
    Start(_) => {
      let suffix_indices = take_suffix_indices(glyphs, text_w)
      let suffix = glyphs_from_indices(glyphs, suffix_indices)
      out_glyphs.push(ellipsis_glyph(line, font_size, 0.0F, e_w))
      source_indices.push(-1)
      let (placed_suffix, _) = reposition_glyphs(suffix, e_w)
      for g in placed_suffix {
        out_glyphs.push(g)
      }
      for idx in suffix_indices {
        source_indices.push(idx)
      }
    }
    Middle(_) => {
      let left_budget = text_w / 2.0F
      let prefix_indices = take_prefix_indices(glyphs, left_budget)
      let prefix = glyphs_from_indices(glyphs, prefix_indices)
      let mut prefix_w = 0.0F
      for g in prefix {
        prefix_w = prefix_w + g.w
      }
      let suffix_indices = take_suffix_indices(
        glyphs,
        nonneg(text_w - prefix_w),
      )
      let suffix = glyphs_from_indices(glyphs, suffix_indices)
      let (placed_prefix, prefix_w2) = reposition_glyphs(prefix, 0.0F)
      for g in placed_prefix {
        out_glyphs.push(g)
      }
      for idx in prefix_indices {
        source_indices.push(idx)
      }
      out_glyphs.push(ellipsis_glyph(line, font_size, prefix_w2, e_w))
      source_indices.push(-1)
      let (placed_suffix, _) = reposition_glyphs(suffix, prefix_w2 + e_w)
      for g in placed_suffix {
        out_glyphs.push(g)
      }
      for idx in suffix_indices {
        source_indices.push(idx)
      }
    }
    None => return line
  }

  let mut out_w = 0.0F
  for g in out_glyphs {
    out_w = out_w + g.w
  }
  let decorations = project_decorations(line.decorations, source_indices)
  let mut out_start = line.start
  let mut out_end = line.end
  let mut seen_real_glyph = false
  for source_i in source_indices {
    if source_i >= 0 && glyphs.get(source_i) is Some(source_glyph) {
      if !seen_real_glyph {
        out_start = source_glyph.start
        seen_real_glyph = true
      }
      out_end = source_glyph.end
    }
  }
  if !seen_real_glyph {
    out_start = line.end
    out_end = line.end
  }
  LayoutLine::{
    ..line,
    start: out_start,
    end: out_end,
    w: out_w,
    glyphs: out_glyphs,
    decorations,
  }
}

///|
fn apply_ellipsize(
  layout : Array[LayoutLine],
  ellipsize : Ellipsize,
  wrap : Wrap,
  width_opt : Float?,
  font_size : Float,
) -> Array[LayoutLine] {
  if layout.length() == 0 || ellipsize is None {
    return layout
  }

  let keep_lines = match ellipsize {
    None => layout.length()
    Start(limit) | Middle(limit) | End(limit) =>
      match (wrap, limit) {
        (Wrap::None, _) => 1
        (_, Lines(lines)) => if lines <= 0 { 1 } else { lines }
        (_, Height(height_limit)) => {
          let mut acc = 0.0F
          let mut kept = 0
          for line in layout {
            let h = layout_line_height_for_ellipsize(line, font_size)
            if acc + h > height_limit {
              break
            }
            acc = acc + h
            kept = kept + 1
          }
          if kept <= 0 {
            1
          } else {
            kept
          }
        }
      }
  }

  let keep = if keep_lines > layout.length() {
    layout.length()
  } else {
    keep_lines
  }
  let target_i = if keep <= 0 { 0 } else { keep - 1 }
  let target = layout[target_i]
  let max_w = if width_opt is Some(w) { w } else { target.w }
  let needs_trim = keep < layout.length() || target.w > max_w
  if !needs_trim {
    return layout
  }

  let out : Array[LayoutLine] = []
  for i in 0.. Array[LayoutLine] {
  let mut layout = if layout0.length() == 0 {
    [empty_layout_line()]
  } else {
    layout0
  }
  let default_align = if rtl { Align::Right } else { Align::Left }
  let align = if align_opt is Some(a) { a } else { default_align }
  let line_width_opt = if width_opt is Some(width) {
    Some(width)
  } else {
    max_layout_width(layout)
  }
  if line_width_opt is Some(width) {
    match align {
      Justified => layout = justify_lines_except_last(text, layout, width)
      _ => layout = apply_align_with_rtl(text, layout, width, align, rtl)
    }
  }
  if hinting is Hinting::Enabled {
    let out : Array[LayoutLine] = []
    for l in layout {
      out.push(hint_line(l))
    }
    layout = out
  }
  layout
}

///|
fn align_eq(a : Align, b : Align) -> Bool {
  match (a, b) {
    (Left, Left) => true
    (Right, Right) => true
    (Center, Center) => true
    (Justified, Justified) => true
    (Start, Start) => true
    (End, End) => true
    _ => false
  }
}

///|
fn align_opt_eq(a : Align?, b : Align?) -> Bool {
  match (a, b) {
    (None, None) => true
    (Some(a1), Some(b1)) => align_eq(a1, b1)
    _ => false
  }
}

///|
pub struct BufferLine {
  text : String
  ending : LineEnding
  attrs_list : AttrsList
  align : Align?
  shape_opt : Cached[ShapeLine]
  layout_opt : Cached[Array[LayoutLine]]
  shaping : Shaping
  metadata : Int?
}

///|
pub fn BufferLine::new(
  text : String,
  ending : LineEnding,
  attrs_list : AttrsList,
  shaping : Shaping,
) -> BufferLine {
  BufferLine::{
    text,
    ending,
    attrs_list,
    align: None,
    shape_opt: Empty,
    layout_opt: Empty,
    shaping,
    metadata: None,
  }
}

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

///|
pub fn BufferLine::into_text(self : BufferLine) -> String {
  self.text
}

///|
pub fn BufferLine::ending(self : BufferLine) -> LineEnding {
  self.ending
}

///|
pub fn BufferLine::set_ending(
  self : BufferLine,
  ending : LineEnding,
) -> BufferLine {
  if ending.as_str() == self.ending.as_str() {
    self
  } else {
    BufferLine::{ ..self, ending, }.reset_shaping()
  }
}

///|
pub fn BufferLine::attrs_list(self : BufferLine) -> AttrsList {
  self.attrs_list
}

///|
pub fn BufferLine::set_attrs_list(
  self : BufferLine,
  attrs_list : AttrsList,
) -> (BufferLine, Bool) {
  if attrs_list == self.attrs_list {
    (self, false)
  } else {
    (BufferLine::{ ..self, attrs_list, }.reset_shaping(), true)
  }
}

///|
pub fn BufferLine::align(self : BufferLine) -> Align? {
  self.align
}

///|
pub fn BufferLine::reset(self : BufferLine) -> BufferLine {
  BufferLine::{ ..self, metadata: None }.reset_shaping()
}

///|
pub fn BufferLine::reset_shaping(self : BufferLine) -> BufferLine {
  let shape_opt = self.shape_opt.set_unused()
  BufferLine::{ ..self, shape_opt, }.reset_layout()
}

///|
pub fn BufferLine::reset_layout(self : BufferLine) -> BufferLine {
  let layout_opt = self.layout_opt.set_unused()
  BufferLine::{ ..self, layout_opt, }
}

///|
pub fn BufferLine::shape_opt(self : BufferLine) -> ShapeLine? {
  self.shape_opt.get()
}

///|
pub fn BufferLine::shape(self : BufferLine, tab_width : Int) -> BufferLine {
  if self.shape_opt.is_unused() {
    let (shape_opt, reused_opt) = self.shape_opt.take_unused()
    let reused = if reused_opt is Some(v) { v } else { ShapeLine::empty() }
    let built = reused.build(
      self.text,
      self.attrs_list,
      self.shaping,
      tab_width,
    )
    let shape_opt = shape_opt.set_used(built)
    BufferLine::{ ..self, shape_opt, }.reset_layout()
  } else {
    self
  }
}

///|
/// Shape using a provided FontSystem (best-effort).
///
/// This is an incremental step towards upstream `cosmic-text`:
/// we keep the same cache behavior, but build glyph advances from the font.
pub fn BufferLine::shape_with_font_system(
  self : BufferLine,
  font_system : FontSystem,
  tab_width : Int,
) -> BufferLine {
  if self.shape_opt.is_unused() {
    let (shape_opt, reused_opt) = self.shape_opt.take_unused()
    let reused = if reused_opt is Some(v) { v } else { ShapeLine::empty() }
    let built = reused.build_with_font_system(
      font_system,
      self.text,
      self.attrs_list,
      self.shaping,
      tab_width,
    )
    let shape_opt = shape_opt.set_used(built)
    BufferLine::{ ..self, shape_opt, }.reset_layout()
  } else {
    self
  }
}

///|
pub fn BufferLine::layout_opt(self : BufferLine) -> Array[LayoutLine]? {
  self.layout_opt.get()
}

///|
pub fn BufferLine::layout(
  self : BufferLine,
  font_size : Float,
  width_opt : Float?,
  wrap : Wrap,
  ellipsize : Ellipsize,
  match_mono_width : Float?,
  tab_width : Int,
  hinting : Hinting,
) -> BufferLine {
  if self.layout_opt.is_unused() {
    let line = self.shape(tab_width)
    let shape_opt = line.shape_opt()
    if shape_opt is Some(shape) {
      let cell_w = if match_mono_width is Some(w) { w } else { font_size }
      let layout = layout_from_shape(
        line.text,
        shape,
        font_size,
        cell_w,
        width_opt,
        wrap,
      )
      let layout = apply_ellipsize(
        layout, ellipsize, wrap, width_opt, font_size,
      )
      let layout = apply_layout_postprocess(
        line.text,
        layout,
        shape.rtl,
        line.align,
        width_opt,
        hinting,
      )
      let (layout_opt, _) = line.layout_opt.take_unused()
      let layout_opt = layout_opt.set_used(layout)
      BufferLine::{ ..line, layout_opt, }
    } else {
      line
    }
  } else {
    self
  }
}

///|
/// Layout using a provided FontSystem (best-effort).
pub fn BufferLine::layout_with_font_system(
  self : BufferLine,
  font_system : FontSystem,
  font_size : Float,
  width_opt : Float?,
  wrap : Wrap,
  ellipsize : Ellipsize,
  match_mono_width : Float?,
  tab_width : Int,
  hinting : Hinting,
) -> BufferLine {
  if self.layout_opt.is_unused() {
    let line = self.shape_with_font_system(font_system, tab_width)
    let shape_opt = line.shape_opt()
    if shape_opt is Some(shape) {
      let cell_w = if match_mono_width is Some(w) { w } else { font_size }
      let layout = layout_from_shape(
        line.text,
        shape,
        font_size,
        cell_w,
        width_opt,
        wrap,
      )
      let layout = apply_ellipsize(
        layout, ellipsize, wrap, width_opt, font_size,
      )
      let layout = apply_layout_postprocess(
        line.text,
        layout,
        shape.rtl,
        line.align,
        width_opt,
        hinting,
      )
      let (layout_opt, _) = line.layout_opt.take_unused()
      let layout_opt = layout_opt.set_used(layout)
      BufferLine::{ ..line, layout_opt, }
    } else {
      line
    }
  } else {
    // Match existing API: if layout is cached, no work.
    // `hinting` is currently kept for API compatibility.
    ignore(hinting)
    self
  }
}

///|
pub fn BufferLine::set_text(
  self : BufferLine,
  text : String,
  ending : LineEnding,
  attrs_list : AttrsList,
) -> (BufferLine, Bool) {
  if text != self.text ||
    ending.as_str() != self.ending.as_str() ||
    !attrs_list.eq(self.attrs_list) {
    let updated = BufferLine::{ ..self, text, ending, attrs_list }.reset()
    (updated, true)
  } else {
    (self, false)
  }
}

///|
pub fn BufferLine::set_align(
  self : BufferLine,
  align : Align?,
) -> (BufferLine, Bool) {
  if align_opt_eq(align, self.align) {
    (self, false)
  } else {
    (BufferLine::{ ..self, align, }.reset_layout(), true)
  }
}

///|
pub fn BufferLine::layout_runs(
  self : BufferLine,
  height_opt : Float?,
  line_height : Float,
) -> Iter[LayoutRun] {
  let runs : Array[LayoutRun] = []
  let shape = match self.shape_opt() {
    None => return runs.iter()
    Some(v) => v
  }
  let layout = match self.layout_opt() {
    None => return runs.iter()
    Some(v) => v
  }
  let mut line_top_acc = 0.0F
  for l in layout {
    let this_line_height = if l.line_height_opt is Some(h) {
      h
    } else {
      line_height
    }
    let line_top = line_top_acc
    let glyph_height = l.max_ascent + l.max_descent
    let centering_offset = (this_line_height - glyph_height) / 2.0F
    let line_y = line_top + centering_offset + l.max_ascent
    if height_opt is Some(h) && line_y - l.max_ascent > h {
      return runs.iter()
    }
    line_top_acc = line_top_acc + this_line_height
    if line_y + l.max_descent < 0.0F {
      continue
    }
    runs.push(LayoutRun::{
      line_i: 0,
      line_y,
      line_top,
      line_height: this_line_height,
      text: self.text,
      rtl: shape.rtl,
      glyphs: l.glyphs,
      decorations: l.decorations,
      line_w: l.w,
    })
  }
  runs.iter()
}

///|
pub fn BufferLine::metadata(self : BufferLine) -> Int? {
  self.metadata
}

///|
pub fn BufferLine::set_metadata(
  self : BufferLine,
  metadata : Int,
) -> BufferLine {
  BufferLine::{ ..self, metadata: Some(metadata) }
}

///|
/// Append `other` to `self` (consumes both, returns updated line).
pub fn BufferLine::append(self : BufferLine, other : BufferLine) -> BufferLine {
  let len = self.text.length()
  let other_text = other.text
  let other_len = other_text.length()
  let mut attrs_list = self.attrs_list
  if other.attrs_list.defaults() != attrs_list.defaults() {
    attrs_list = attrs_list.add_span(
      len,
      len + other_len,
      other.attrs_list.defaults(),
    )
  }
  for span in other.attrs_list.spans_iter() {
    attrs_list = attrs_list.add_span(
      span.start + len,
      span.end + len,
      span.attrs,
    )
  }
  BufferLine::{
    ..self,
    text: string_concat(self.text, other_text),
    ending: other.ending,
    attrs_list,
  }.reset()
}

///|
/// Split off new line at index. Returns (left, right).
pub fn BufferLine::split_off(
  self : BufferLine,
  index : Int,
) -> (BufferLine, BufferLine) {
  let index = if index < 0 { 0 } else { index }
  let end = self.text.length()
  let idx = if index > end { end } else { index }
  let left_text = substring(self.text, 0, idx)
  let right_text = substring(self.text, idx, end)
  let (left_attrs, right_attrs) = self.attrs_list.split_off(idx)
  let left = BufferLine::{
    ..self,
    text: left_text,
    ending: None,
    attrs_list: left_attrs,
  }.reset()
  let mut right = BufferLine::new(
    right_text,
    self.ending,
    right_attrs,
    self.shaping,
  )
  right = BufferLine::{ ..right, align: self.align }
  (left, right)
}