///|
struct Completion {
  code : Int
  level : Int
  value : TclValue
  options : Array[(String, TclValue)]
  skip_trace : Bool
} derive(Debug)

///|
pub struct Evaluation {
  code : Int
  result : String
  options : String
} derive(Debug)

///|
fn Completion::actual_code(self : Completion) -> Int {
  if self.level != 0 {
    2
  } else {
    self.code
  }
}

///|
fn option_get(options : Array[(String, TclValue)], name : String) -> String? {
  for (key, value) in options {
    if key == name {
      return Some(value.text)
    }
  }
  None
}

///|
fn option_set_value(
  options : Array[(String, TclValue)],
  name : String,
  value : TclValue,
) -> Unit {
  for i in 0.. TclValue raise TclError {
  let options = self.options.copy()
  option_set(options, "-code", self.code.to_string())
  option_set(options, "-level", self.level.to_string())
  dictionary_value(options.map(p => (text_value(p.0), p.1)))
}

///|
fn completion(
  code : Int,
  value : String,
  level? : Int = 0,
  options? : Array[(String, TclValue)] = [],
  skip_trace? : Bool = false,
) -> Completion {
  { code, level, value: text_value(value), options, skip_trace, }
}

///|
fn completion_error(
  message : String,
  errorcode? : String = "NONE",
) -> Completion {
  completion(1, message, options=[("-errorcode", text_value(errorcode))])
}

///|
fn outcome(error : TclError) -> Completion {
  match error {
    Signal(result) => result
    Invalid(message) => completion_error(message)
    Return(value) => completion(0, value, level=1)
    Break => completion(3, "")
    Continue => completion(4, "")
  }
}

///|
fn completion_code(value : String) -> Int raise TclError {
  match value {
    "ok" => 0
    "error" => 1
    "return" => 2
    "break" => 3
    "continue" => 4
    _ =>
      integer(value) catch {
        _ =>
          raise Signal(
            completion_error(
              "bad completion code \"" +
              value +
              "\": must be ok, error, return, break, continue, or an integer",
              errorcode="TCL RESULT ILLEGAL_CODE",
            ),
          )
      }
  }
}

///|
fn Interpreter::propagate_value(
  self : Interpreter,
  result : Completion,
) -> TclValue raise TclError {
  if result.actual_code() == 0 {
    self.state.return_options.val = result.options
    result.value
  } else {
    raise Signal(result)
  }
}

///|
fn Interpreter::record_error(self : Interpreter, result : Completion) -> Unit {
  if result.actual_code() != 1 {
    return
  }
  let info = option_get(result.options, "-errorinfo").unwrap_or(
    result.value.text,
  )
  let code = option_get(result.options, "-errorcode").unwrap_or("NONE")
  self.state.error_stack.val = option_get(result.options, "-errorstack").unwrap_or(
    "",
  )
  for (name, value) in [("::errorInfo", info), ("::errorCode", code)] {
    match self.state.globals.get(name) {
      Some(cell) => cell.value = Some(Scalar(text_value(value)))
      None =>
        self.state.globals[name] = {
          value: Some(Scalar(text_value(value))),
          declared: false,
          frame_local: false,
        }
    }
  }
}

///|
fn Interpreter::annotate_error(
  self : Interpreter,
  result : Completion,
  source : String,
  line : Int,
  args : Array[String],
) -> Completion raise TclError {
  if result.actual_code() != 1 {
    return result
  }
  let options = result.options.copy()
  let previous = option_get(options, "-errorinfo").unwrap_or("")
  let text = if result.skip_trace && !previous.is_empty() {
    previous
  } else if previous.is_empty() {
    result.value.text + "\n    while executing\n\"" + source + "\""
  } else {
    previous + "\n    invoked from within\n\"" + source + "\""
  }
  // Limit diagnostics independently of the value and output quotas.
  if text.length() > 1000000 {
    raise Invalid("error diagnostic size limit")
  }
  option_set(options, "-errorinfo", text)
  option_set(options, "-errorline", line.to_string())
  if option_get(options, "-errorcode") is None {
    option_set(options, "-errorcode", "NONE")
  }
  if option_get(options, "-errorstack") is None {
    option_set(
      options,
      "-errorstack",
      if result.skip_trace {
        ""
      } else {
        format_list(["INNER", format_list(args)])
      },
    )
  }
  let annotated = { ..result, options, skip_trace: false, }
  self.record_error(annotated)
  annotated
}

///|
fn Interpreter::capture_value(
  self : Interpreter,
  source : TclValue,
  depth : Int,
) -> Completion {
  self.state.return_options.val = []
  let result = try {
    let value = self.execute_value_script(source, depth)
    {
      ..completion(0, value.text, options=self.state.return_options.val),
      value,
    }
  } catch {
    error => outcome(error)
  }
  self.state.return_options.val = []
  if result.actual_code() == 1 {
    if option_get(result.options, "-errorinfo") is None {
      option_set(result.options, "-errorinfo", result.value.text)
    }
    if option_get(result.options, "-errorline") is None {
      option_set(result.options, "-errorline", "1")
    }
    if option_get(result.options, "-errorstack") is None {
      option_set(result.options, "-errorstack", "")
    }
  }
  self.record_error(result)
  result
}

///|
// Raw script completion, like Tcl catch, without consuming a return level.
// An invalid budget still raises an API usage error.
pub fn Interpreter::eval_catch(
  self : Interpreter,
  source : String,
  budget? : Int = 10000,
) -> Evaluation raise TclError {
  if budget < 1 || budget > 1000000 {
    raise Invalid("budget range")
  }
  self.budget.val = budget
  let result = self.capture(source, 0)
  {
    code: result.actual_code(),
    result: result.value.text,
    options: result.option_text(),
  }
}

///|
fn merge_return_options(
  target : Array[(String, TclValue)],
  values : Array[(String, TclValue)],
  depth : Int,
) -> Unit raise TclError {
  if depth > 64 {
    raise Invalid("return option nesting limit")
  }
  for (key, value) in values {
    if key == "-options" {
      merge_return_options(
        target,
        dictionary_values(value).map(p => (p.0.text, p.1)),
        depth + 1,
      )
    } else {
      option_set_value(target, key, value)
    }
  }
}

///|
fn Interpreter::return_command(
  self : Interpreter,
  values : Array[TclValue],
) -> TclValue raise TclError {
  let args = values.map(v => v.text)
  if args.length() <= 2 {
    if args.length() == 1 {
      raise Return("")
    }
    raise Signal({
      ..completion(0, "", level=1),
      value: if values.length() == 2 {
        values[1]
      } else {
        text_value("")
      },
    })
  }
  let count = args.length() - 1
  let value = if count % 2 == 1 {
    values[values.length() - 1]
  } else {
    text_value("")
  }
  let pairs = []
  for i = 1; i + 1 < args.length(); i = i + 2 {
    pairs.push((args[i], values[i + 1]))
  }
  let options = []
  merge_return_options(options, pairs, 0)
  let mut code = completion_code(option_get(options, "-code").unwrap_or("0"))
  let level_text = option_get(options, "-level").unwrap_or("1")
  let mut level = try {
    let parsed = integer(level_text)
    if parsed < 0 {
      raise Invalid("negative level")
    }
    parsed
  } catch {
    _ =>
      raise Signal(
        completion_error(
          "bad -level value: expected non-negative integer but got \"" +
          level_text +
          "\"",
          errorcode="TCL RESULT ILLEGAL_LEVEL",
        ),
      )
  }
  if code == 2 {
    code = 0
    level += 1
  }
  let extras = options.filter(pair => pair.0 != "-code" && pair.0 != "-level")
  if option_get(extras, "-errorcode") is Some(text) {
    ignore(parse_list(text))
  }
  if option_get(extras, "-errorstack") is Some(text) {
    if parse_list(text).length() % 2 != 0 {
      raise Invalid("errorstack requires token/value pairs")
    }
  }
  if code == 1 {
    if option_get(extras, "-errorcode") is None {
      option_set(extras, "-errorcode", "NONE")
    }
    if option_get(extras, "-errorinfo") is Some(text) &&
      !text.is_empty() &&
      option_get(extras, "-errorline") is None {
      option_set(extras, "-errorline", "1")
    }
  }
  self.propagate_value({
    ..completion(
      code,
      value.text,
      level~,
      options=extras,
      skip_trace=level == 0 &&
        option_get(extras, "-errorinfo").unwrap_or("") != "",
    ),
    value,
  })
}

///|
fn Interpreter::error_command(
  self : Interpreter,
  values : Array[TclValue],
) -> TclValue raise TclError {
  let args = values.map(v => v.text)
  self.state.return_options.val = []
  let n = args.length()
  if args[0] == "throw" {
    if n != 3 {
      raise Invalid("throw requires error type and message")
    }
    if parse_list(args[1]).is_empty() {
      raise Signal(
        completion_error(
          "type must be non-empty list",
          errorcode="TCL OPERATION THROW BADEXCEPTION",
        ),
      )
    }
    raise Signal({
      ..completion_error(args[2], errorcode=args[1]),
      value: values[2],
    })
  }
  if n < 2 || n > 4 {
    raise Invalid("error arity")
  }
  let options = []
  if n >= 3 {
    options.push(("-errorinfo", values[2]))
  }
  options.push(
    ("-errorcode", if n == 4 { values[3] } else { text_value("NONE") }),
  )
  raise Signal({
    ..completion(1, args[1], options~, skip_trace=n >= 3 && !args[2].is_empty()),
    value: values[1],
  })
}

///|
fn Interpreter::catch_command(
  self : Interpreter,
  input : Array[TclValue],
  depth : Int,
) -> String raise TclError {
  let args = input.map(v => v.text)
  if args.length() < 2 || args.length() > 4 {
    raise Invalid("catch arity")
  }
  let result = self.capture_value(input[1], depth + 1)
  if args.length() >= 3 {
    self.set_value(args[2], result.value)
  }
  if args.length() == 4 {
    self.set_value(args[3], result.option_value())
  }
  result.actual_code().to_string()
}

///|
fn Interpreter::capture(
  self : Interpreter,
  source : String,
  depth : Int,
) -> Completion {
  self.capture_value(text_value(source), depth)
}

///|
fn option_set(
  options : Array[(String, TclValue)],
  name : String,
  value : String,
) -> Unit {
  option_set_value(options, name, text_value(value))
}

///|
fn Completion::option_text(self : Completion) -> String raise TclError {
  self.option_value().text
}