///|
/// Errors produced while parsing or writing terminfo data.
pub suberror ParseError {
  /// The file does not begin with a terminfo magic number (0o432 or 0o1036).
  BadMagic(Int)
  /// The file ends before all sections declared by the header were read.
  Truncated
  /// A header count is inconsistent with the file size.
  BadCount(String)
  /// A string-table offset points outside its table.
  BadOffset(String)
  /// A `use=` chain contains a cycle.
  CyclicUse(String)
  /// A source entry contains malformed syntax (bad escape, bad number, ...).
  BadSyntax(String)
}

///|
/// Errors produced when expanding a parameterized capability string.
pub suberror ExpandError {
  /// A `%` sequence is malformed (truncated or unknown conversion).
  BadPercent(String)
  /// An operator popped from an empty stack, or a `%s` index is out of range.
  StackUnderflow(String)
  /// `%t`, `%e` or `%;` appears without a matching `%?`.
  Unbalanced(String)
}

///|
/// A parsed terminal description.
///
/// Standard capabilities are stored in arrays aligned with `bool_names`,
/// `num_names` and `str_names`, so parse/serialize round-trips are lossless
/// even for capabilities this library does not know by name. Extended
/// (user-defined) capabilities carry their own names and are stored in maps.
///
/// String capabilities are raw byte strings, exactly as stored on disk:
/// escape sequences and `%` parameter references are kept verbatim. Use
/// `tparm` to expand the latter.
pub struct Entry {
  /// Terminal names: primary name first, verbose name last.
  mut names : Array[String]
  /// Standard booleans, aligned with `bool_names`. 0 = absent/false, 1 = true, 2 = canceled.
  booleans : Array[Int]
  /// Standard numbers, aligned with `num_names`. -1 = absent, -2 = canceled.
  numbers : Array[Int]
  /// Standard strings, aligned with `str_names`. `None` = absent or canceled.
  strings : Array[Bytes?]
  /// Extended (user-defined) booleans, by name. 0 = false, 1 = true, 2 = canceled.
  ext_booleans : Map[String, Int]
  /// Extended (user-defined) numbers, by name. -1 = absent, -2 = canceled.
  ext_numbers : Map[String, Int]
  /// Extended (user-defined) strings, by name. Only present values.
  ext_strings : Map[String, Bytes]
  /// Names of extended strings whose value was canceled in the source.
  ext_canceled_strings : Array[String]
}

///|
/// One entry parsed from terminfo source format (the `terminfo.src` language).
/// Unlike a compiled `Entry`, capabilities are keyed by name directly and
/// string values keep their source escapes (use `unescape_value` to expand
/// them). Capability keys may be either long names (`columns`) or
/// two-character codes (`co`); both resolve to the same binary index when
/// compiling.
pub struct SourceEntry {
  /// Terminal names: primary name first, verbose name last.
  names : Array[String]
  /// Boolean capabilities, by name. 0 = false, 1 = true, 2 = canceled.
  booleans : Map[String, Int]
  /// Numeric capabilities, by name. -1/-2 sentinels preserved as written.
  numbers : Map[String, Int]
  /// String capabilities, by name, with source escapes intact.
  strings : Map[String, String]
  /// Targets of `use=` fields, in order of appearance.
  uses : Array[String]
  /// Names of string capabilities canceled with `name@` (standard or extended).
  canceled : Array[String]
}

///|
pub fn Entry::names(self : Entry) -> Array[String] {
  self.names
}

///|
/// The primary (first) terminal name.
pub fn Entry::primary_name(self : Entry) -> String {
  self.names[0]
}

///|
/// The verbose (last) name, as shown by `infocmp -d` style tools.
pub fn Entry::longname(self : Entry) -> String {
  self.names[self.names.length() - 1]
}

///|
fn Entry::bool_at(self : Entry, i : Int) -> Bool? {
  if i < self.booleans.length() {
    match self.booleans[i] {
      0 => Some(false)
      1 => Some(true)
      _ => None
    }
  } else {
    None
  }
}

///|
fn Entry::num_at(self : Entry, i : Int) -> Int? {
  if i < self.numbers.length() {
    let v = self.numbers[i]
    if v < 0 {
      None
    } else {
      Some(v)
    }
  } else {
    None
  }
}

///|
fn Entry::str_at(self : Entry, i : Int) -> Bytes? {
  if i < self.strings.length() {
    self.strings[i]
  } else {
    None
  }
}

///|
/// Look up a Boolean capability by long name (e.g. `auto_right_margin`) or
/// two-character terminfo code (e.g. `am`). Returns `None` when the
/// capability is absent or canceled.
pub fn Entry::get_bool(self : Entry, name : String) -> Bool? {
  if self.ext_booleans.contains(name) {
    let v = self.ext_booleans[name]
    return if v == 0 { Some(false) } else if v == 1 { Some(true) } else { None }
  }
  match bool_index.get(name) {
    Some(i) => Entry::bool_at(self, i)
    None =>
      match bool_code_index.get(name) {
        Some(i) => Entry::bool_at(self, i)
        None => None
      }
  }
}

///|
/// Look up a numeric capability by long name (e.g. `columns`) or code
/// (e.g. `cols`). Returns `None` when absent or canceled.
pub fn Entry::get_num(self : Entry, name : String) -> Int? {
  if self.ext_numbers.contains(name) {
    let v = self.ext_numbers[name]
    return if v < 0 { None } else { Some(v) }
  }
  match num_index.get(name) {
    Some(i) => Entry::num_at(self, i)
    None =>
      match num_code_index.get(name) {
        Some(i) => Entry::num_at(self, i)
        None => None
      }
  }
}

///|
/// Look up a string capability by long name (e.g. `cursor_address`) or code
/// (e.g. `cup`, `sgr0`). Returns `None` when absent or canceled. The value
/// is returned verbatim: escapes resolved, `%` sequences and `$<...>`
/// padding intact.
pub fn Entry::get_str(self : Entry, name : String) -> Bytes? {
  if self.ext_canceled_strings.contains(name) {
    return None
  }
  if self.ext_strings.contains(name) {
    return Some(self.ext_strings[name])
  }
  match str_index.get(name) {
    Some(i) => Entry::str_at(self, i)
    None =>
      match str_code_index.get(name) {
        Some(i) => Entry::str_at(self, i)
        None => None
      }
  }
}

///|
/// Look up a Boolean capability by its two-character terminfo code (e.g. `am`).
pub fn Entry::get_bool_code(self : Entry, code : String) -> Bool? {
  match bool_code_index.get(code) {
    Some(i) => Entry::bool_at(self, i)
    None => None
  }
}

///|
/// Look up a numeric capability by its two-character terminfo code (e.g. `co`).
pub fn Entry::get_num_code(self : Entry, code : String) -> Int? {
  match num_code_index.get(code) {
    Some(i) => Entry::num_at(self, i)
    None => None
  }
}

///|
/// Look up a string capability by its two-character terminfo code (e.g. `AF`).
pub fn Entry::get_str_code(self : Entry, code : String) -> Bytes? {
  match str_code_index.get(code) {
    Some(i) => Entry::str_at(self, i)
    None => None
  }
}

///|
/// An empty entry with standard-section arrays sized to the full
/// capability tables. Used as a base when compiling source entries.
pub fn empty() -> Entry {
  {
    names: [],
    booleans: Array::make(bool_names.length(), 0),
    numbers: Array::make(num_names.length(), -1),
    strings: Array::make(str_names.length(), None),
    ext_booleans: Map([]),
    ext_numbers: Map([]),
    ext_strings: Map([]),
    ext_canceled_strings: [],
  }
}