///|
fn string_choice(
  value : String,
  choices : Array[String],
) -> String raise TclError {
  if choices.contains(value) {
    return value
  }
  let matches = choices.filter(choice => {
    !value.is_empty() && choice.has_prefix(value)
  })
  if matches.length() != 1 {
    raise Invalid("unknown or ambiguous string option/class: " + value)
  }
  matches[0]
}

///|
// Return the end of the longest legal numeric prefix, including surrounding
// ASCII whitespace. A missing prefix fails at zero, including after a sign.
fn numeric_prefix(text : String, real : Bool) -> Int {
  let cs = utf16_units(text)
  let n = cs.length()
  let mut start = 0
  while start < n && list_space(cs[start]) {
    start += 1
  }
  if start < n && (cs[start] == '+' || cs[start] == '-') {
    start += 1
  }
  let mut end = 0
  if start < n && cs[start] >= '0' && cs[start] <= '9' {
    let mut base = 10
    let mut i = start
    if cs[start] == '0' {
      end = start + 1
      base = 8
      if start + 1 < n {
        match cs[start + 1] {
          'x' | 'X' => {
            base = 16
            i = start + 2
          }
          'b' | 'B' => {
            base = 2
            i = start + 2
          }
          'o' | 'O' => {
            base = 8
            i = start + 2
          }
          _ => ()
        }
      }
    }
    while i < n && hex_digit(cs[i]) >= 0 && hex_digit(cs[i]) < base {
      i += 1
      end = i
    }
  }
  if real {
    let mut i = start
    while i < n && cs[i] >= '0' && cs[i] <= '9' {
      i += 1
    }
    let mut digits = i - start
    let mut floating = false
    if i < n && cs[i] == '.' {
      floating = true
      i += 1
      while i < n && cs[i] >= '0' && cs[i] <= '9' {
        i += 1
        digits += 1
      }
    }
    if digits > 0 {
      if floating {
        end = end.max(i)
      }
      if i < n && (cs[i] == 'e' || cs[i] == 'E') {
        i += 1
        if i < n && (cs[i] == '+' || cs[i] == '-') {
          i += 1
        }
        let exponent = i
        while i < n && cs[i] >= '0' && cs[i] <= '9' {
          i += 1
        }
        if i > exponent {
          end = end.max(i)
        }
      }
    }
    let rest = unicode_case(unit_slice(text, start, n), 0)
    if rest.has_prefix("inf") {
      end = start + (if rest.has_prefix("infinity") { 8 } else { 3 })
    } else if rest.has_prefix("nan") {
      end = start + 3
      if end < n && cs[end] == '(' {
        let mut i = end + 1
        while i < n && hex_digit(cs[i]) >= 0 {
          i += 1
        }
        let count = i - end - 1
        if count > 0 && count <= 13 && i < n && cs[i] == ')' {
          end = i + 1
        }
      }
    }
  }
  if end > 0 {
    while end < n && list_space(cs[end]) {
      end += 1
    }
  }
  end
}

///|
fn Interpreter::string_is(
  self : Interpreter,
  input : Array[TclValue],
) -> String raise TclError {
  let args = input.map(v => v.text)
  let n = args.length()
  if n < 4 || n > 7 {
    raise Invalid("string is arity")
  }
  let classes = [
    "alnum", "alpha", "ascii", "control", "digit", "graph", "lower", "print", "punct",
    "space", "upper", "wordchar", "xdigit",
  ]
  let kind = string_choice(
    args[2],
    classes +
    [
      "integer", "wideinteger", "entier", "double", "boolean", "true", "false", "list",
    ],
  )
  let mut strict = false
  let mut failure = None
  let mut i = 3
  while i < n - 1 {
    match string_choice(args[i], ["-strict", "-failindex"]) {
      "-strict" => {
        strict = true
        i += 1
      }
      _ => {
        if i + 1 >= n - 1 {
          raise Invalid("missing string is failure variable")
        }
        failure = Some(args[i + 1])
        i += 2
      }
    }
  }
  let value = args[n - 1]
  let position = Ref(0)
  let valid = if value.is_empty() {
    !strict || kind == "list"
  } else if classes.search(kind) is Some(bit) {
    input[n - 1].payload = Plain
    let mut valid = true
    for c in value.to_array() {
      if (unicode_mask(c.to_int()) & (1 << bit)) == 0 {
        valid = false
        break
      }
      position.val += 1
    }
    valid
  } else {
    match kind {
      "integer" | "wideinteger" | "entier" | "double" => {
        position.val = numeric_prefix(value, kind == "double")
        if position.val != value.length() {
          false
        } else if kind == "double" {
          true
        } else {
          position.val = -1
          try {
            let v = whole(value)
            if kind == "integer" {
              v >= -4294967295N && v <= 4294967295N
            } else if kind == "wideinteger" {
              v >= -18446744073709551615N && v <= 18446744073709551615N
            } else {
              true
            }
          } catch {
            _ => false
          }
        }
      }
      "boolean" | "true" | "false" =>
        try {
          let v = string_boolean(value)
          kind == "boolean" || v == (kind == "true")
        } catch {
          _ => false
        }
      "list" =>
        try {
          ignore(parse_list_at(value, Some(position)))
          true
        } catch {
          _ => false
        }
      _ => false
    }
  }
  if !valid && failure is Some(name) {
    self.set_var(name, position.val.to_string())
  }
  if valid && kind == "list" {
    ignore(input[n - 1].as_list())
  }
  if valid && ["integer", "wideinteger", "entier", "double"].contains(kind) {
    ignore(input[n - 1].as_number())
  }
  boolean_text(valid)
}