///|
/// Rendering policy for tabs and non-ASCII source text.
pub struct DisplayConfig {
  tab_width : Int
  ambiguous_width : Int
} derive(Eq, Debug)

///|
pub fn DisplayConfig::default() -> DisplayConfig {
  { tab_width: 4, ambiguous_width: 1 }
}

///|
pub fn DisplayConfig::new(
  tab_width : Int,
  ambiguous_width : Int,
) -> DisplayConfig {
  {
    tab_width: if tab_width < 1 {
      1
    } else {
      tab_width
    },
    ambiguous_width: if ambiguous_width == 2 {
      2
    } else {
      1
    },
  }
}

///|
pub fn DisplayConfig::tab_width(self : DisplayConfig) -> Int {
  self.tab_width
}

///|
fn utf8_sequence_length(first : Byte) -> Int {
  let value = first.to_int()
  if value < 0x80 {
    1
  } else if value < 0xe0 {
    2
  } else if value < 0xf0 {
    3
  } else {
    4
  }
}

///|
fn decode_code_point(bytes : Bytes, offset : Int) -> (Int, Int) {
  let first = bytes[offset].to_int()
  let count = utf8_sequence_length(bytes[offset])
  if count == 1 || offset + count > bytes.length() {
    return (first, 1)
  }
  let mut value = first & (0x7f >> count)
  let mut index = 1
  while index < count {
    let next = bytes[offset + index].to_int()
    if (next & 0xc0) != 0x80 {
      return (0xfffd, 1)
    }
    value = (value << 6) | (next & 0x3f)
    index += 1
  }
  (value, count)
}

///|
fn is_combining(code : Int) -> Bool {
  (code >= 0x0300 && code <= 0x036f) ||
  (code >= 0x1ab0 && code <= 0x1aff) ||
  (code >= 0x1dc0 && code <= 0x1dff) ||
  (code >= 0x20d0 && code <= 0x20ff) ||
  (code >= 0xfe00 && code <= 0xfe0f) ||
  (code >= 0xfe20 && code <= 0xfe2f) ||
  code == 0x200d
}

///|
fn is_wide(code : Int) -> Bool {
  code >= 0x1100 &&
  (
    code <= 0x115f ||
    code == 0x2329 ||
    code == 0x232a ||
    (code >= 0x2e80 && code <= 0xa4cf && code != 0x303f) ||
    (code >= 0xac00 && code <= 0xd7a3) ||
    (code >= 0xf900 && code <= 0xfaff) ||
    (code >= 0xfe10 && code <= 0xfe19) ||
    (code >= 0xfe30 && code <= 0xfe6f) ||
    (code >= 0xff00 && code <= 0xff60) ||
    (code >= 0xffe0 && code <= 0xffe6) ||
    (code >= 0x1f300 && code <= 0x1faff) ||
    (code >= 0x20000 && code <= 0x3fffd)
  )
}

///|
fn is_ambiguous(code : Int) -> Bool {
  (code >= 0x00a1 && code <= 0x00ff) ||
  (code >= 0x2010 && code <= 0x2027) ||
  (code >= 0x2190 && code <= 0x21ff) ||
  (code >= 0x2460 && code <= 0x24ff) ||
  (code >= 0x2500 && code <= 0x259f)
}

///|
fn code_point_width(code : Int, config : DisplayConfig) -> Int {
  if code == 0 || is_combining(code) {
    0
  } else if code < 0x20 || (code >= 0x7f && code < 0xa0) {
    0
  } else if is_wide(code) {
    2
  } else if is_ambiguous(code) {
    config.ambiguous_width
  } else {
    1
  }
}

///|
/// Returns the number of terminal cells occupied by UTF-8 text.
pub fn display_width(
  text : String,
  config? : DisplayConfig = DisplayConfig::default(),
) -> Int {
  let bytes = @utf8.encode(text)
  let mut column = 0
  let mut offset = 0
  while offset < bytes.length() {
    if bytes[offset] == 9 {
      column += config.tab_width - column % config.tab_width
      offset += 1
    } else {
      let (code, count) = decode_code_point(bytes, offset)
      column += code_point_width(code, config)
      offset += count
    }
  }
  column
}

///|
/// Converts a UTF-8 byte offset into a terminal-cell column.
pub fn display_column(
  text : String,
  byte_offset : Int,
  config? : DisplayConfig = DisplayConfig::default(),
) -> Int {
  let bytes = @utf8.encode(text)
  let limit = if byte_offset < 0 {
    0
  } else if byte_offset > bytes.length() {
    bytes.length()
  } else {
    byte_offset
  }
  let mut column = 0
  let mut offset = 0
  while offset < limit {
    if bytes[offset] == 9 {
      column += config.tab_width - column % config.tab_width
      offset += 1
    } else {
      let (code, count) = decode_code_point(bytes, offset)
      column += code_point_width(code, config)
      offset += count
    }
  }
  column
}

///|
/// Replaces tabs with spaces while preserving tab-stop alignment.
pub fn expand_tabs(
  text : String,
  config? : DisplayConfig = DisplayConfig::default(),
) -> String {
  let bytes = @utf8.encode(text)
  let output = StringBuilder::new()
  let mut column = 0
  let mut offset = 0
  while offset < bytes.length() {
    if bytes[offset] == 9 {
      let count = config.tab_width - column % config.tab_width
      let mut spaces = 0
      while spaces < count {
        output.write_char(' ')
        spaces += 1
      }
      column += count
      offset += 1
    } else {
      let (code, count) = decode_code_point(bytes, offset)
      output.write_string(@utf8.decode_lossy(bytes[offset:offset + count]))
      column += code_point_width(code, config)
      offset += count
    }
  }
  output.to_string()
}