///|
enum CommandBody {
  Native(String)
  Script(Procedure)
  Imported(Command)
  EnsembleCommand(Ensemble)
} derive(Debug)

///|
// Imports retain command identity across rename and replacement. Deletion
// removes the command and every import that depends on it.
struct Command {
  mut name : String
  mut body : CommandBody
} derive(Debug)

///|
fn command_name(prefix : String, name : String) -> String {
  let result = qualified_name(prefix, name)
  if name.is_empty() || name.has_suffix("::") {
    if result == "::" {
      result
    } else {
      result + "::"
    }
  } else {
    result
  }
}

///|
fn command_parent(name : String) -> String {
  if name.has_suffix("::") && name != "::" {
    name[:name.length() - 2].to_owned()
  } else {
    namespace_parent(name)
  }
}

///|
fn Command::origin(self : Command) -> Command {
  let mut cursor = self
  while cursor.body is Imported(target) {
    cursor = target
  }
  cursor
}

///|
fn Command::depends_on(self : Command, target : Command) -> Bool {
  let mut cursor = self
  while true {
    if physical_equal(cursor, target) {
      return true
    }
    match cursor.body {
      Imported(parent) => cursor = parent
      _ => return false
    }
  } nobreak {
    false
  }
}

///|
fn Interpreter::find_command(self : Interpreter, name : String) -> Command? {
  let key = command_name(self.frame.namespace_name, name)
  if self.state.commands.get(key) is Some(command) {
    return Some(command)
  }
  if !name.has_prefix("::") {
    if self.state.namespace_paths.get(self.frame.namespace_name) is Some(path) {
      for space in path {
        if self.state.commands.get(command_name(space, name)) is Some(command) {
          return Some(command)
        }
      }
    }
    return self.state.commands.get(command_name("::", name))
  }
  None
}

///|
fn Interpreter::define_command(
  self : Interpreter,
  name : String,
  body : CommandBody,
) -> Unit raise TclError {
  match self.state.commands.get(name) {
    Some(command) => command.body = body
    None => {
      if self.state.commands.length() >= 10000 {
        raise Invalid("command count limit")
      }
      self.state.commands[name] = { name, body, }
    }
  }
}

///|
fn Interpreter::delete_command(self : Interpreter, command : Command) -> Unit {
  let doomed = self.state.commands
    .values()
    .filter(c => c.depends_on(command))
    .to_array()
  for item in doomed {
    self.state.commands.remove(item.name)
  }
}

///|
fn Interpreter::rename_command(
  self : Interpreter,
  old : String,
  new : String,
) -> String raise TclError {
  let command = match self.find_command(old) {
    Some(command) => command
    None => raise Invalid("unknown command")
  }
  if new.is_empty() {
    self.delete_command(command)
  } else {
    let target = command_name(self.frame.namespace_name, new)
    if self.state.commands.contains(target) {
      raise Invalid("target command exists")
    }
    let owner = command_parent(target)
    if !self.state.namespaces.contains(owner) {
      raise Invalid("target namespace does not exist")
    }
    self.state.commands.remove(command.name)
    command.name = target
    if command.body is Script(definition) {
      command.body = Script({ ..definition, namespace_name: owner, })
    }
    self.state.commands[target] = command
  }
  ""
}

///|
fn Interpreter::exported_commands(
  self : Interpreter,
  space : String,
) -> Array[Command] raise TclError {
  let patterns = self.state.exports.get(space).unwrap_or([])
  let result = []
  for command in self.state.commands.values() {
    if command_parent(command.name) == space {
      for pattern in patterns {
        if glob_match(pattern, namespace_tail(command.name), false) {
          result.push(command)
          break
        }
      }
    }
  }
  result
}

///|
fn Interpreter::command_pattern(
  self : Interpreter,
  pattern : String,
) -> (String, String) raise TclError {
  let full = command_name(self.frame.namespace_name, pattern)
  let space = command_parent(full)
  if !self.state.namespaces.contains(space) {
    raise Invalid("unknown namespace in command pattern")
  }
  (space, namespace_tail(full))
}

///|
fn Interpreter::namespace_import(
  self : Interpreter,
  args : Array[String],
  depth : Int,
) -> String raise TclError {
  let force = args.length() > 2 && args[2] == "-force"
  let start = if force { 3 } else { 2 }
  if args.length() == start {
    if force {
      return ""
    }
    let names = self.state.commands
      .values()
      .filter(c => {
        command_parent(c.name) == self.frame.namespace_name &&
        c.body is Imported(_)
      })
      .map(c => namespace_tail(c.name))
      .to_array()
    names.sort()
    return format_list(names)
  }
  for pattern in args[start:] {
    if self.state.commands.contains("::auto_import") {
      ignore(self.command(["::auto_import", pattern], depth + 1))
    }
    if !pattern.contains("::") {
      raise Invalid("import pattern requires namespace")
    }
    let (space, tail) = self.command_pattern(pattern)
    if space == self.frame.namespace_name {
      raise Invalid("cannot import from own namespace")
    }
    for command in self.exported_commands(space) {
      self.tick()
      if !glob_match(tail, namespace_tail(command.name), false) {
        continue
      }
      let target = command_name(
        self.frame.namespace_name,
        namespace_tail(command.name),
      )
      if self.state.commands.get(target) is Some(existing) {
        if existing.body is Imported(previous) &&
          physical_equal(previous, command) {
          continue
        }
        if !force {
          raise Invalid("import command conflict")
        }
        if command.depends_on(existing) {
          raise Invalid("import cycle")
        }
      }
      self.define_command(target, Imported(command))
    }
  }
  ""
}

///|
fn Interpreter::namespace_forget(
  self : Interpreter,
  args : Array[String],
) -> String raise TclError {
  for pattern in args[2:] {
    let qualified = pattern.contains("::")
    let (space, tail) = if qualified {
      self.command_pattern(pattern)
    } else {
      ("", pattern)
    }
    for command in self.state.commands.values().to_array() {
      if command_parent(command.name) != self.frame.namespace_name {
        continue
      }
      if command.body is Imported(parent) {
        let mut matched = !qualified &&
          glob_match(tail, namespace_tail(command.name), false)
        let mut cursor = parent
        while qualified {
          if command_parent(cursor.name) == space &&
            glob_match(tail, namespace_tail(cursor.name), false) {
            matched = true
            break
          }
          match cursor.body {
            Imported(next) => cursor = next
            _ => break
          }
        }
        if matched {
          self.delete_command(command)
        }
      }
    }
  }
  ""
}

///|
fn Interpreter::command_names(
  self : Interpreter,
  pattern : String,
  procedures_only : Bool,
) -> Array[String] raise TclError {
  let qualified = pattern.contains("::")
  let full = command_name(self.frame.namespace_name, pattern)
  let space = command_parent(full)
  let tail = if qualified { namespace_tail(full) } else { pattern }
  let scopes = if qualified {
    [space]
  } else {
    let scopes = [self.frame.namespace_name]
    if !procedures_only {
      for
        item in self.state.namespace_paths
        .get(self.frame.namespace_name)
        .unwrap_or([]) {
        scopes.push(item)
      }
      scopes.push("::")
    }
    scopes
  }
  let values : Array[String] = []
  for command in self.state.commands.values() {
    if scopes.contains(command_parent(command.name)) &&
      (!procedures_only || command.origin().body is Script(_)) &&
      glob_match(tail, namespace_tail(command.name), false) {
      let value = if qualified {
        command.name
      } else {
        namespace_tail(command.name)
      }
      if !values.contains(value) {
        values.push(value)
      }
    }
  }
  values.sort()
  values
}

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