///|
/// Characters that occupy no column on screen: combining marks, joiners,
/// variation selectors and skin tone modifiers.
fn is_zero_width(cp : Int32) -> Bool {
  (cp >= 0x0300 && cp <= 0x036F) ||
  (cp >= 0x200B && cp <= 0x200F) ||
  (cp >= 0xFE00 && cp <= 0xFE0F) ||
  (cp >= 0x1F3FB && cp <= 0x1F3FF) ||
  cp == 0xFEFF
}

///|
/// East Asian Wide and Fullwidth ranges, which take two columns.
fn is_wide(cp : Int32) -> Bool {
  (cp >= 0x1100 && cp <= 0x115F) ||
  (cp >= 0x2E80 && cp <= 0x303E) ||
  (cp >= 0x3041 && cp <= 0x33FF) ||
  (cp >= 0x3400 && cp <= 0x4DBF) ||
  (cp >= 0x4E00 && cp <= 0x9FFF) ||
  (cp >= 0xA000 && cp <= 0xA4CF) ||
  (cp >= 0xAC00 && cp <= 0xD7A3) ||
  (cp >= 0xF900 && cp <= 0xFAFF) ||
  (cp >= 0xFE30 && cp <= 0xFE6F) ||
  (cp >= 0xFF00 && cp <= 0xFF60) ||
  (cp >= 0xFFE0 && cp <= 0xFFE6) ||
  (cp >= 0x1F300 && cp <= 0x1F64F) ||
  (cp >= 0x1F900 && cp <= 0x1F9FF) ||
  (cp >= 0x20000 && cp <= 0x3FFFD)
}

///|
/// Number of terminal columns a single character occupies.
pub fn char_width(c : Char) -> Int32 {
  let cp = c.to_int()
  if cp < 0x20 || cp == 0x7F {
    0
  } else if is_zero_width(cp) {
    0
  } else if is_wide(cp) {
    2
  } else {
    1
  }
}

///|
/// Escape scanner states shared by `display_width` and `truncate_visible`.
///
/// `0` is plain text, `1` follows `ESC`, `2` is inside a CSI sequence, `3` is
/// inside an OSC sequence and `4` is the `ESC` that ends one.
fn next_escape_state(state : Int32, cp : Int32) -> Int32 {
  if state == 0 {
    if cp == 0x1B {
      1
    } else {
      0
    }
  } else if state == 1 {
    if cp == '['.to_int() {
      2
    } else if cp == ']'.to_int() {
      3
    } else {
      0
    }
  } else if state == 2 {
    if cp >= 0x40 && cp <= 0x7E {
      0
    } else {
      2
    }
  } else if state == 3 {
    if cp == 0x07 {
      0
    } else if cp == 0x1B {
      4
    } else {
      3
    }
  } else {
    0
  }
}

///|
/// Number of terminal columns `s` occupies, ignoring ANSI escape sequences.
pub fn display_width(s : String) -> Int32 {
  let mut w = 0
  let mut state = 0
  for c in s {
    if state == 0 {
      w = w + char_width(c)
    }
    state = next_escape_state(state, c.to_int())
  }
  w
}

///|
/// Cut `s` down to at most `max` columns, appending an ellipsis when something
/// was dropped. Escape sequences pass through untouched and a style left open
/// by the cut is closed.
pub fn truncate_visible(s : String, max : Int32) -> String {
  if max <= 0 {
    return ""
  }
  if display_width(s) <= max {
    return s
  }
  let budget = if max > 1 { max - 1 } else { max }
  let out = StringBuilder()
  let mut w = 0
  let mut state = 0
  let mut styled = false
  for c in s {
    let cp = c.to_int()
    if state != 0 || cp == 0x1B {
      styled = true
      out.write_char(c)
    } else {
      let cw = char_width(c)
      if w + cw > budget {
        break
      }
      w = w + cw
      out.write_char(c)
    }
    state = next_escape_state(state, cp)
  }
  out.write_string("…")
  if styled {
    out.write_string("\u001b[0m")
  }
  out.to_string()
}

///|
/// `s` repeated `n` times.
fn repeat(s : String, n : Int32) -> String {
  let out = StringBuilder()
  let mut i = 0
  while i < n {
    out.write_string(s)
    i = i + 1
  }
  out.to_string()
}