///|
priv enum GlobToken {
  Many
  One
  Character(Char)
  Range(Array[(Char, Char)])
}

///|
fn glob_match(
  pattern : String,
  value : String,
  nocase : Bool,
  codepoints? : Bool = false,
) -> Bool raise TclError {
  let units = if codepoints { collection_codepoints } else { utf16_units }
  let p = units(if nocase { unicode_case(pattern, 0) } else { pattern })
  let s = units(if nocase { unicode_case(value, 0) } else { value })
  if p.length() > 4096 ||
    s.length() > 100000 ||
    p.length() > 1000000 / (s.length() + 1) {
    raise Invalid("glob work limit")
  }
  let tokens = []
  let mut i = 0
  while i < p.length() {
    let c = p[i]
    i += 1
    if c == '*' {
      tokens.push(Many)
    } else if c == '?' {
      tokens.push(One)
    } else if c == '\\' {
      if i == p.length() {
        return false
      }
      tokens.push(Character(p[i]))
      i += 1
    } else if c == '[' {
      let ranges = []
      let mut closed = false
      while i < p.length() {
        if p[i] == ']' {
          i += 1
          closed = true
          break
        }
        let first = p[i]
        i += 1
        if i + 1 < p.length() && p[i] == '-' && p[i + 1] != ']' {
          ranges.push((first, p[i + 1]))
          i += 2
        } else {
          ranges.push((first, first))
        }
      }
      if !closed {
        return false
      }
      tokens.push(Range(ranges))
    } else {
      tokens.push(Character(c))
    }
  }
  let mut previous = Array::make(s.length() + 1, false)
  previous[0] = true
  for token in tokens {
    let next = Array::make(s.length() + 1, false)
    if token is Many {
      next[0] = previous[0]
    }
    for j in 1..<=s.length() {
      next[j] = match token {
        Many => previous[j] || next[j - 1]
        One => previous[j - 1]
        Character(c) => previous[j - 1] && s[j - 1] == c
        Range(ranges) =>
          previous[j - 1] &&
          ranges
          .iter()
          .any(pair => {
            (s[j - 1] >= pair.0 && s[j - 1] <= pair.1) ||
            (s[j - 1] >= pair.1 && s[j - 1] <= pair.0)
          })
      }
    }
    previous = next
  }
  previous[s.length()]
}

///|
fn Interpreter::string_command(
  self : Interpreter,
  input : Array[TclValue],
) -> TclValue raise TclError {
  let args = input.map(v => v.text)
  let n = args.length()
  if n < 2 {
    raise Invalid("string arity")
  }
  let op = args[1]
  let text = match op {
    "cat" => {
      if n == 2 {
        return text_value("")
      }
      let mut result = input[2]
      for item in input[3:] {
        if !item.text.is_empty() {
          if item.text.length() > 1000000 - result.text.length() {
            raise Invalid("string cat size limit")
          }
          result = text_value(result.text + item.text)
        }
      }
      return result
    }
    "length" => {
      if n != 3 {
        raise Invalid("string length arity")
      }
      input[2].payload = Plain
      args[2].length().to_string()
    }
    "index" | "range" => {
      if n != (if op == "index" { 4 } else { 5 }) {
        raise Invalid("string index/range arity")
      }
      input[2].payload = Plain
      let text = args[2]
      let first = list_index(args[3], text.length())
      let last = if op == "index" {
        first
      } else {
        list_index(args[4], text.length())
      }
      if (op == "index" && first < 0) ||
        first >= text.length() ||
        last < first ||
        last < 0 {
        ""
      } else {
        unit_slice(text, first.max(0), last.min(text.length() - 1) + 1)
      }
    }
    "compare" | "equal" => {
      let mut i = 2
      let mut nocase = false
      let mut limit = None
      while i < n - 2 {
        if args[i] == "-nocase" {
          nocase = true
          i += 1
        } else if args[i] == "-length" && i + 1 < n - 2 {
          limit = Some(integer(args[i + 1]))
          i += 2
        } else {
          raise Invalid("string compare option")
        }
      }
      if i != n - 2 {
        raise Invalid("string compare arity")
      }
      let a = if nocase { unicode_case(args[i], 0) } else { args[i] }
      let b = if nocase { unicode_case(args[i + 1], 0) } else { args[i + 1] }
      let a = match limit {
        Some(v) if v >= 0 => unit_slice(a, 0, v.min(a.length()))
        _ => a
      }
      let b = match limit {
        Some(v) if v >= 0 => unit_slice(b, 0, v.min(b.length()))
        _ => b
      }
      let cmp = tcl_string_compare(a, b)
      if op == "equal" {
        boolean_text(cmp == 0)
      } else if cmp < 0 {
        "-1"
      } else if cmp > 0 {
        "1"
      } else {
        "0"
      }
    }
    "first" | "last" => {
      if n != 4 && n != 5 {
        raise Invalid("string search arity")
      }
      input[2].payload = Plain
      input[3].payload = Plain
      let needle = args[2]
      let text = args[3]
      if needle.is_empty() {
        return text_value("-1")
      }
      if op == "first" {
        let start = if n == 5 {
          list_index(args[4], text.length()).max(0)
        } else {
          0
        }
        if start > text.length() {
          return text_value("-1")
        }
        match unit_slice(text, start, text.length()).find(needle) {
          Some(i) => (i + start).to_string()
          None => "-1"
        }
      } else {
        let last = if n == 5 {
          list_index(args[4], text.length()).min(text.length() - 1)
        } else {
          text.length() - 1
        }
        if last < 0 {
          return text_value("-1")
        }
        unit_slice(text, 0, last + 1).rev_find(needle).unwrap_or(-1).to_string()
      }
    }
    "tolower" | "toupper" | "totitle" => {
      if n < 3 || n > 5 {
        raise Invalid("string case arity")
      }
      let text = args[2]
      let first = if n >= 4 { list_index(args[3], text.length()) } else { 0 }
      let last = if n == 5 {
        list_index(args[4], text.length()).min(text.length() - 1)
      } else if n == 4 {
        first.min(text.length() - 1)
      } else {
        text.length() - 1
      }
      let first = first.max(0).min(text.length())
      if last < first {
        return text_value(text)
      }
      let part = unit_slice(text, first, last + 1)
      let converted = unicode_case(
        part,
        if op == "tolower" {
          0
        } else if op == "toupper" {
          1
        } else {
          2
        },
        conversion=true,
      )
      unit_slice(text, 0, first) +
      converted +
      unit_slice(text, last + 1, text.length())
    }
    "trim" | "trimleft" | "trimright" => {
      if n != 3 && n != 4 {
        raise Invalid("string trim arity")
      }
      let text = args[2].to_array()
      let chars = if n == 4 { Some(args[3].to_array()) } else { None }
      let matches = fn(c) {
        match chars {
          Some(values) => values.contains(c)
          None => c.to_int() <= 65535 && (unicode_mask(c.to_int()) & 512) != 0
        }
      }
      let mut first = 0
      let mut last = text.length()
      if op != "trimright" {
        while first < last && matches(text[first]) {
          first += 1
        }
      }
      if op != "trimleft" {
        while last > first && matches(text[last - 1]) {
          last -= 1
        }
      }
      String::from_array(text[first:last])
    }
    "repeat" => {
      if n != 4 {
        raise Invalid("string repeat arity")
      }
      let count = integer(args[3]).max(0)
      if count == 1 {
        return input[2]
      }
      if !args[2].is_empty() && count > 1000000 / args[2].length() {
        raise Invalid("string repeat size")
      }
      args[2].repeat(count)
    }
    "reverse" => {
      if n != 3 {
        raise Invalid("string reverse arity")
      }
      input[2].payload = Plain
      unicode_reverse(args[2])
    }
    "replace" => {
      if n != 5 && n != 6 {
        raise Invalid("string replace arity")
      }
      input[2].payload = Plain
      let text = args[2]
      let first = list_index(args[3], text.length()).max(0)
      let last = list_index(args[4], text.length()).min(text.length() - 1)
      if first >= text.length() || last < first {
        return text_value(text)
      }
      unit_slice(text, 0, first) +
      (if n == 6 { args[5] } else { "" }) +
      unit_slice(text, last + 1, text.length())
    }
    "match" => {
      let nocase = n == 5 && args[2] == "-nocase"
      if n != (if nocase { 5 } else { 4 }) {
        raise Invalid("string match arity")
      }
      boolean_text(glob_match(args[n - 2], args[n - 1], nocase))
    }
    "map" => {
      let nocase = n == 5 && args[2] == "-nocase"
      if n != (if nocase { 5 } else { 4 }) {
        raise Invalid("string map arity")
      }
      let mapping = parse_list(args[n - 2])
      if mapping.length() % 2 != 0 {
        raise Invalid("string map expects pairs")
      }
      if mapping.is_empty() {
        return input[n - 1]
      }
      input[n - 1].payload = Plain
      let source = args[n - 1]
      let subject = if nocase { unicode_case(source, 0) } else { source }
      let out = StringBuilder()
      let mut i = 0
      let mut size = 0
      while i < source.length() {
        let mut matched = false
        let mut j = 0
        while j < mapping.length() {
          let key = if nocase {
            unicode_case(mapping[j], 0)
          } else {
            mapping[j]
          }
          if !key.is_empty() &&
            unit_slice(subject, i, subject.length()).has_prefix(key) {
            size += mapping[j + 1].length()
            if size > 1000000 {
              raise Invalid("string map size limit")
            }
            out.write_string(mapping[j + 1])
            i += key.length()
            matched = true
            break
          }
          j += 2
        }
        if !matched {
          out.write_string(unit_slice(source, i, i + 1))
          i += 1
          size += 1
          if size > 1000000 {
            raise Invalid("string map size limit")
          }
        }
      }
      out.to_string()
    }
    "is" => self.string_is(input)
    _ => raise Invalid("unsupported string subcommand " + op)
  }
  text_value(text)
}

///|
fn string_boolean(value : String) -> Bool raise TclError {
  let text = unicode_case(value, 0)
  if text == "0" {
    return false
  }
  if text == "1" {
    return true
  }
  if text.is_empty() || text == "o" {
    raise Invalid("expected boolean")
  }
  for word in ["true", "yes", "on"] {
    if word.has_prefix(text) {
      return true
    }
  }
  for word in ["false", "no", "off"] {
    if word.has_prefix(text) {
      return false
    }
  }
  raise Invalid("expected boolean")
}

///|
fn Interpreter::info_command(
  self : Interpreter,
  args : Array[String],
) -> String raise TclError {
  let n = args.length()
  if n < 2 {
    raise Invalid("info arity")
  }
  match args[1] {
    "script" => {
      if n == 3 {
        self.state.script_name.val = args[2]
      } else if n != 2 {
        raise Invalid("info script ?filename?")
      }
      self.state.script_name.val
    }
    "complete" => {
      if n != 3 {
        raise Invalid("info complete arity")
      }
      boolean_text(is_complete(args[2]))
    }
    "exists" => {
      if n != 3 {
        raise Invalid("info exists arity")
      }
      let exists = match self.binding(args[2], false) {
        Some(binding) =>
          if binding.index is None {
            binding.cell.value is Some(_)
          } else {
            try (binding.read() is Some(_)) catch {
              _ => false
            }
          }
        None => false
      }
      boolean_text(exists)
    }
    "vars" | "globals" | "locals" => {
      if n > 3 {
        raise Invalid("info vars arity")
      }
      let values = []
      if self.frame.procedure && args[1] != "globals" {
        for key, binding in self.frame.vars {
          if binding.cell.value is Some(_) &&
            (args[1] != "locals" || !binding.linked) {
            values.push(key)
          }
        }
      } else {
        let prefix = if args[1] == "globals" {
          "::"
        } else {
          self.frame.namespace_name
        }
        for key, cell in self.state.globals {
          if (cell.value is Some(_) || cell.declared) &&
            namespace_parent(key) == prefix {
            values.push(namespace_tail(key))
          }
        }
        for key, binding in self.state.aliases {
          if binding.cell.value is Some(_) &&
            namespace_parent(key) == prefix &&
            !values.contains(namespace_tail(key)) {
            values.push(namespace_tail(key))
          }
        }
      }
      values.sort()
      format_list(
        if n == 3 {
          values.filter(key => glob_match(args[2], key, false))
        } else {
          values
        },
      )
    }
    "procs" | "commands" => {
      if n > 3 {
        raise Invalid("info commands arity")
      }
      format_list(
        self.command_names(
          if n == 3 {
            args[2]
          } else {
            "*"
          },
          args[1] == "procs",
        ),
      )
    }
    "body" | "args" | "default" => {
      if n != (if args[1] == "default" { 5 } else { 3 }) {
        raise Invalid("info procedure arity")
      }
      let definition = match self.find_command(args[2]) {
        Some(command) =>
          match command.origin().body {
            Script(p) => p
            _ => raise Invalid("not a procedure")
          }
        None => raise Invalid("unknown procedure")
      }
      if args[1] == "body" {
        definition.body
      } else if args[1] == "args" {
        format_list(definition.parameters.map(p => p.0))
      } else {
        for (name, value) in definition.parameters {
          if name == args[3] {
            self.set_value(args[4], value.unwrap_or(text_value("")))
            return boolean_text(value is Some(_))
          }
        }
        raise Invalid("unknown argument")
      }
    }
    "level" => {
      if n != 2 {
        raise Invalid("info level argument not implemented")
      }
      let mut level = 0
      let mut frame = self.frame
      while frame.parent is Some(parent) {
        level += 1
        frame = parent
      }
      level.to_string()
    }
    "errorstack" => {
      if n != 2 && !(n == 3 && args[2].is_empty()) {
        raise Invalid("info errorstack arity or unsupported interpreter")
      }
      self.state.error_stack.val
    }
    "patchlevel" => {
      if n != 2 {
        raise Invalid("info patchlevel arity")
      }
      "0.9.0"
    }
    "tclversion" => {
      if n != 2 {
        raise Invalid("info tclversion arity")
      }
      "8.6"
    }
    _ => raise Invalid("unsupported info subcommand")
  }
}

///|
// MoonBit String.compare is shortlex. Tcl uses lexicographic UTF-16 units.
fn tcl_string_compare(a : String, b : String) -> Int {
  for i in 0.. y {
      return 1
    }
  }
  a.length().compare(b.length())
}