///|
/// String layout utilities for terminal rendering.
///
/// All width calculations operate on visible characters only — ANSI escape
/// sequences are excluded from width counts.

///|
/// Returns true if the character terminates a CSI escape sequence (A–Z or a–z).
fn is_csi_terminator(ch : Char) -> Bool {
  (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
}

///|
fn skip_osc_sequence(chars : Array[Char], start : Int) -> Int {
  let n = chars.length()
  let mut i = start
  while i < n {
    if chars[i] == '\u0007' {
      return i + 1
    }
    if chars[i] == '\u001b' && i + 1 < n && chars[i + 1] == '\\' {
      return i + 2
    }
    i = i + 1
  }
  start
}

///|
/// Remove recognized ANSI escape sequences from `s`.
///
/// Handles CSI sequences (`ESC [` … letter), OSC sequences (`ESC ]` … BEL/ST),
/// and bare two-char sequences (`ESC` followed by any single non-`[`/`]`
/// character).
pub fn strip_ansi(s : String) -> String {
  let chars = s.to_array()
  let n = chars.length()
  let buf = StringBuilder::new(size_hint=n)
  let mut i = 0
  while i < n {
    let ch = chars[i]
    if ch == '\u001b' {
      i = i + 1
      if i < n {
        let next = chars[i]
        if next == '[' {
          // CSI sequence: skip until (and including) the terminating letter.
          i = i + 1
          while i < n && !is_csi_terminator(chars[i]) {
            i = i + 1
          }
          if i < n {
            i = i + 1
          }
        } else if next == ']' {
          i = i + 1
          i = skip_osc_sequence(chars, i)
        } else {
          // Single char after ESC — skip it.
          i = i + 1
        }
      }
    } else {
      buf.write_char(ch)
      i = i + 1
    }
  }
  buf.to_string()
}

///|
/// Return the visible terminal-cell width of `s`, ignoring ANSI escape
/// sequences and treating wide/combining/grapheme clusters as terminal cells.
pub fn visible_width(s : String) -> Int {
  @internal.display_width(s)
}

///|
/// Split `s` on newline characters.
///
/// `"a\nb\n"` → `["a", "b", ""]`
/// `""`       → `[""]`
pub fn split_lines(s : String) -> Array[String] {
  let arr : Array[String] = []
  let mut line_buf = StringBuilder::new(size_hint=64)
  s
  .iter()
  .each(fn(ch) {
    if ch == '\n' {
      arr.push(line_buf.to_string())
      line_buf = StringBuilder::new(size_hint=64)
    } else {
      line_buf.write_char(ch)
    }
  })
  arr.push(line_buf.to_string())
  arr
}

///|
/// Return the number of lines in `s` (equivalent to `split_lines(s).length()`).
pub fn line_count(s : String) -> Int {
  let mut count = 1
  for ch in s {
    if ch == '\n' {
      count = count + 1
    }
  }
  count
}

///|
/// Return the maximum visible width across all lines in `s`.
/// Returns 0 for an empty string.
pub fn max_line_width(s : String) -> Int {
  let mut max = 0
  split_lines(s).each(fn(line) {
    let w = visible_width(line)
    if w > max {
      max = w
    }
  })
  max
}

///|
/// Center `s` within a field of `width` visible characters by padding with
/// spaces on both sides.  Returns `s` unchanged if its visible width is
/// already ≥ `width`.
pub fn center(s : String, width : Int) -> String {
  let sw = visible_width(s)
  if sw >= width {
    return s
  }
  let total_pad = width - sw
  let left_pad = total_pad / 2
  let right_pad = total_pad - left_pad
  String::make(left_pad, ' ') + s + String::make(right_pad, ' ')
}

///|
/// Left-pad `s` with spaces until its visible width equals `width`.
/// Returns `s` unchanged if its visible width is already ≥ `width`.
pub fn pad_left(s : String, width : Int) -> String {
  let sw = visible_width(s)
  if sw >= width {
    return s
  }
  String::make(width - sw, ' ') + s
}

///|
/// Right-pad `s` with spaces until its visible width equals `width`.
/// Returns `s` unchanged if its visible width is already ≥ `width`.
pub fn pad_right(s : String, width : Int) -> String {
  @internal.pad_right_width(s, width)
}

///|
/// Truncate `s` so that its visible width is at most `max_width`.
///
/// ANSI-aware: escape sequences are never split mid-sequence and do not
/// themselves consume any visible width.
pub fn truncate(s : String, max_width : Int) -> String {
  @internal.truncate_width(s, max_width)
}