///|
pub suberror TclError {
  Invalid(String)
  Return(String)
  Break
  Continue
  Signal(Completion)
} derive(Debug)

///|
pub struct Interpreter {
  state : State
  frame : Frame
  output : Array[String]
  output_size : Array[Int]
  budget : Ref[Int]
} derive(Debug)

///|
pub fn Interpreter::new() -> Interpreter {
  let commands : Map[String, Command] = Map([])
  for name in builtin_names() {
    let key = "::" + name
    commands[key] = { name: key, body: Native(name), }
  }
  {
    state: {
      globals: Map([]),
      aliases: Map([]),
      commands,
      exports: Map([]),
      namespace_paths: Map([]),
      unknown_handlers: Map([]),
      namespaces: Map([("::", true)]),
      cache: ParseCache::new(),
      return_options: Ref([]),
      error_stack: Ref(""),
      host_io: { invoke: None, },
      script_name: Ref(""),
      packages: Map([]),
      package_scripts: Map([]),
      package_loading: [],
    },
    frame: {
      vars: Map([]),
      namespace_name: "::",
      procedure: false,
      parent: None,
    },
    output: [],
    output_size: [0],
    budget: Ref(10000),
  }
}

///|
pub fn Interpreter::printed(self : Interpreter) -> String {
  let value = self.output.join("")
  if value.has_suffix("\n") {
    value[:value.length() - 1].to_owned()
  } else {
    value
  }
}

///|
pub fn Interpreter::output_text(self : Interpreter) -> String {
  self.output.join("")
}

///|
pub fn Interpreter::clear_output(self : Interpreter) -> Unit {
  self.output.clear()
  self.output_size[0] = 0
}

///|
fn integer(s : String) -> Int raise TclError {
  if small_decimal(s) is Some(value) {
    return value
  }
  let n = whole(s)
  if n < -2147483648N || n > 2147483647N {
    raise Invalid("bounded integer required")
  }
  n.to_int()
}

///|
fn builtin_names() -> Array[String] {
  [
    "set", "unset", "incr", "append", "expr", "puts", "if", "proc", "return", "list",
    "llength", "lindex", "lappend", "join", "split", "concat", "lrange", "lreverse",
    "lrepeat", "lset", "linsert", "lreplace", "lsearch", "lsort", "while", "for",
    "foreach", "lmap", "break", "continue", "catch", "error", "namespace", "global",
    "variable", "upvar", "uplevel", "array", "dict", "string", "info", "eval", "subst",
    "rename", "try", "throw", "format", "scan", "switch", "regexp", "regsub", "source",
    "open", "close", "read", "gets", "seek", "tell", "eof", "flush", "fconfigure",
    "file", "pwd", "cd", "package",
  ]
}

///|
fn Interpreter::command_value(
  self : Interpreter,
  values : Array[TclValue],
  depth : Int,
  discard_result? : Bool = false,
) -> TclValue raise TclError {
  let requested = values[0].text
  self.tick()
  if depth > 64 {
    raise Invalid("command recursion limit")
  }
  let command = match self.find_command(requested) {
    Some(command) => command.origin()
    None => {
      let local_handler = self.state.unknown_handlers
        .get(self.frame.namespace_name)
        .unwrap_or(text_value(""))
      let global_handler = self.state.unknown_handlers
        .get("::")
        .unwrap_or(text_value("::unknown"))
      let handler = if !local_handler.text.is_empty() {
        local_handler
      } else if !global_handler.text.is_empty() {
        global_handler
      } else {
        text_value("::unknown")
      }
      let words = handler.as_list()
      if words.is_empty() ||
        (words[0].text == "::unknown" && self.find_command("::unknown") is None) {
        raise Signal(
          completion_error(
            "invalid command name \"" + requested + "\"",
            errorcode=format_list(["TCL", "LOOKUP", "COMMAND", requested]),
          ),
        )
      }
      return self.command_value(words + values, depth + 1, discard_result~)
    }
  }
  let name = match command.body {
    Script(definition) =>
      return self.invoke_procedure(definition, values, depth)
    EnsembleCommand(ensemble) =>
      return self.invoke_ensemble(
        command,
        ensemble,
        values,
        depth,
        discard_result~,
      )
    Native(name) => name
    Imported(_) => raise Invalid("invalid command import")
  }
  // Only renamed/qualified native commands need a canonical command word.
  // The caller owns the array, so normalize a copy only in that case.
  let values = if requested == name {
    values
  } else {
    let normalized = values.copy()
    normalized[0] = text_value(name)
    normalized
  }
  let n = values.length()
  let text = match name {
    "package" => return self.package_command(values, depth)
    "source"
    | "open"
    | "close"
    | "read"
    | "gets"
    | "seek"
    | "tell"
    | "eof"
    | "flush"
    | "fconfigure"
    | "file"
    | "pwd"
    | "cd" => return self.io_command(values, depth)
    "list"
    | "llength"
    | "lindex"
    | "lappend"
    | "join"
    | "split"
    | "concat"
    | "lrange"
    | "lreverse"
    | "lrepeat" => return self.list_command(values, discard_result~)
    "lset" | "linsert" | "lreplace" | "lsearch" | "lsort" =>
      return self.extended_list_command(values, depth)
    "while"
    | "for"
    | "foreach"
    | "lmap"
    | "break"
    | "continue"
    | "catch"
    | "error"
    | "try"
    | "throw" => return self.control_command(values, depth)
    "namespace" => return self.namespace_command(values, depth)
    "switch" => return self.switch_command(values, depth, discard_result~)
    "regexp" | "regsub" => return self.regexp_command(values)
    "global" | "variable" | "upvar" | "uplevel" =>
      return self.scope_command(values, depth)
    "array" => return self.array_command(values)
    "dict" => return self.dict_command(values, depth, discard_result~)
    "format" => format_command(values)
    "scan" => return self.scan_command(values.map(v => v.text))
    "string" => return self.string_command(values)
    "info" => self.info_command(values.map(v => v.text))
    "set" =>
      if n == 2 {
        return self.read_value(values[1].text)
      } else if n == 3 {
        self.set_value(values[1].text, values[2])
        return values[2]
      } else {
        raise Invalid("set arity")
      }
    "unset" => {
      let mut i = 1
      let mut quiet = false
      if i < n && values[i].text == "-nocomplain" {
        quiet = true
        i += 1
      }
      if i < n && values[i].text == "--" {
        i += 1
      }
      if i == n && !quiet {
        raise Invalid("unset arity")
      }
      for name in values[i:] {
        self.unset_var(name.text, quiet)
      }
      ""
    }
    "incr" => {
      if n != 2 && n != 3 {
        raise Invalid("incr arity")
      }
      let text = integer_add(
        self.get_var(values[1].text).unwrap_or("0"),
        if n == 3 {
          values[2].text
        } else {
          "1"
        },
      )
      self.set_var(values[1].text, text)
      text
    }
    "append" => {
      if n < 2 {
        raise Invalid("append arity")
      }
      let original = self.get_value(values[1].text).unwrap_or(text_value(""))
      if values[2:].iter().all(v => v.text.is_empty()) {
        self.set_value(values[1].text, original)
        return original
      }
      let previous = original.text
      let pieces = [previous] + values[2:].iter().map(v => v.text).collect()
      let mut size = 0
      for value in pieces {
        size += value.length()
        if size > 1000000 {
          raise Invalid("append size limit")
        }
      }
      let result = pieces.join("")
      self.set_var(values[1].text, result)
      result
    }
    "expr" => {
      if n < 2 {
        raise Invalid("expr arity")
      }
      let source = if n == 2 {
        values[1].text
      } else {
        values[1:].iter().map(v => v.text).collect().join(" ")
      }
      return self.expression_object(source, depth)
    }
    "puts" => {
      let nonewline = n >= 2 && values[1].text == "-nonewline"
      let start = if nonewline { 2 } else { 1 }
      if n == start + 2 && values[start].text != "stdout" {
        return text_value(self.host_call("puts", values[1:].map(v => v.text)))
      }
      let value = if n == start + 1 {
        values[start].text
      } else if n == start + 2 && values[start].text == "stdout" {
        values[start + 1].text
      } else {
        raise Invalid("puts arity or unsupported channel")
      }
      let text = value + (if nonewline { "" } else { "\n" })
      if text.length() > 1000000 - self.output_size[0] {
        raise Invalid("printed output size limit")
      }
      self.output_size[0] += text.length()
      self.output.push(text)
      ""
    }
    "if" => {
      let mut i = 1
      while i < n {
        let expression = values[i].text
        i += 1
        if i < n && values[i].text == "then" {
          i += 1
        }
        if i >= n {
          raise Invalid("if missing body")
        }
        i += 1
        if boolean(self.expression_value(expression, depth)) {
          return self.execute_value_script(
            values[i - 1],
            depth + 1,
            discard_result~,
          )
        }
        if i == n {
          return text_value("")
        }
        if values[i].text == "elseif" {
          i += 1
          continue
        }
        if values[i].text == "else" {
          i += 1
        }
        if i != n - 1 {
          raise Invalid("if malformed else")
        }
        return self.execute_value_script(values[i], depth + 1, discard_result~)
      }
      raise Invalid("if arity")
    }
    "proc" => {
      if n != 4 {
        raise Invalid("proc name parameters body")
      }
      let key = command_name(self.frame.namespace_name, values[1].text)
      let owner = command_parent(key)
      if !self.state.namespaces.contains(owner) {
        raise Invalid("procedure namespace missing")
      }
      let parameters = []
      for spec in values[2].as_list() {
        let parts = spec.as_list()
        if parts.is_empty() || parts.length() > 2 {
          raise Invalid("invalid procedure parameter")
        }
        parameters.push(
          (
            parts[0].text,
            if parts.length() == 2 {
              Some(parts[1])
            } else {
              None
            },
          ),
        )
      }
      self.define_command(
        key,
        Script({ parameters, body: values[3].text, namespace_name: owner, }),
      )
      ""
    }
    "return" => return self.return_command(values)
    "eval" => {
      if n < 2 {
        raise Invalid("eval arity")
      }
      return self.execute_value_script(
        script_arguments(values[1:].to_owned()),
        depth + 1,
        discard_result~,
      )
    }
    "subst" => self.subst_command(values.map(v => v.text), depth)
    "rename" => {
      if n != 3 {
        raise Invalid("rename arity")
      }
      self.rename_command(values[1].text, values[2].text)
    }
    _ => raise Invalid("unsupported command " + name)
  }
  text_value(text)
}

///|
pub fn Interpreter::eval(
  self : Interpreter,
  source : String,
  budget? : Int = 10000,
) -> String raise TclError {
  if budget < 1 || budget > 1000000 {
    raise Invalid("budget range")
  }
  self.budget.val = budget
  let result = self.capture(source, 0)
  let result = if result.level != 0 {
    { ..result, level: result.level - 1, }
  } else {
    result
  }
  match result.actual_code() {
    0 => {
      self.state.return_options.val = result.options
      result.value.text
    }
    1 => {
      self.record_error(result)
      raise Invalid(result.value.text)
    }
    3 => raise Invalid("invoked break outside of a loop")
    4 => raise Invalid("invoked continue outside of a loop")
    code => raise Invalid("command returned bad code: " + code.to_string())
  }
}

///|
fn Interpreter::command(
  self : Interpreter,
  args : Array[String],
  depth : Int,
) -> String raise TclError {
  self.command_value(args.map(text_value), depth).text
}