///|
pub(all) struct ReloadStateSelection {
  module_name : String
  globals : Array[String]
}

///|
pub fn ReloadStateSelection::new(
  module_name : String,
  globals : Array[String],
) -> ReloadStateSelection {
  { module_name, globals: globals.copy(), }
}

///|
pub(all) struct ReloadStateArchive {
  module_name : String
  archive : String
}

///|
fn reload_failure(module_name : String, message : String) -> ProgramRunResult {
  ProgramFailed([invalid_vm_state(message, module_name)], [])
}

///|
fn reload_visit_dependency(
  name : String,
  names : Array[String],
  sources : Array[ModuleSource],
  states : Array[Int],
  ordered : Array[String],
) -> String? {
  let mut index = -1
  for candidate_index = 0
      candidate_index < names.length()
      candidate_index = candidate_index + 1 {
    if names[candidate_index] == name {
      index = candidate_index
    }
  }
  if index < 0 {
    return None
  }
  if states[index] == 2 {
    return None
  }
  if states[index] == 1 {
    return Some("cyclic reload dependency involving '\{name}'")
  }
  states[index] = 1
  let source = match find_module_source(sources, name) {
    Some(value) => value
    None => return Some("reload module '\{name}' has no source")
  }
  let statements = match parse_program(source) {
    ProgramParsed(value) => value
    ProgramParseFailed(issues) =>
      return Some(
        "reload module '\{name}' does not parse: \{issues[0].message}",
      )
  }
  for statement in statements {
    match statement {
      ImportStatement(specifier, _, _) =>
        if names.contains(specifier) {
          match
            reload_visit_dependency(specifier, names, sources, states, ordered) {
            Some(message) => return Some(message)
            None => ()
          }
        }
      _ => ()
    }
  }
  states[index] = 2
  ordered.push(name)
  None
}

///|
fn reload_dependency_order(
  names : Array[String],
  sources : Array[ModuleSource],
) -> Result[Array[String], String] {
  let states = Array::make(names.length(), 0)
  let ordered : Array[String] = []
  for name in names {
    match reload_visit_dependency(name, names, sources, states, ordered) {
      Some(message) => return Err(message)
      None => ()
    }
  }
  Ok(ordered)
}

///|
/// Atomically replace several modules and migrate full MWS1 state graphs. The
/// migration callback may validate or transform each archive before it enters
/// the candidate runtime. No old runtime state changes until all replacements,
/// migrations, dependent roots, and limits succeed.
pub fn Vm::reload_modules_with_migration(
  self : Vm,
  replacements : Array[ModuleSource],
  preserve : Array[ReloadStateSelection],
  migrate : (ReloadStateArchive) -> Result[String, String],
) -> ProgramRunResult {
  if self.closed {
    return reload_failure("", "VM is closed")
  }
  if replacements.is_empty() {
    return reload_failure("", "at least one replacement is required")
  }
  self.prepare_operation()
  let current = self.runtime()
  current.begin_operation()
  let candidate_sources = copy_sources(self.sources)
  let targets : Array[String] = []
  for replacement in replacements {
    if replacement.name.length() == 0 {
      return reload_failure("", "replacement module name is required")
    }
    if replacement.source.length() > self.limits.max_source_characters {
      return reload_failure(
        replacement.name,
        "replacement source exceeds the source size limit",
      )
    }
    if targets.contains(replacement.name) {
      return reload_failure(replacement.name, "duplicate replacement module")
    }
    targets.push(replacement.name)
    match find_source_index(candidate_sources, replacement.name) {
      Some(index) => candidate_sources[index] = replacement
      None => candidate_sources.push(replacement)
    }
  }
  let archives : Array[ReloadStateArchive] = []
  let migration_targets : Array[String] = []
  for selection in preserve {
    for existing in archives {
      if existing.module_name == selection.module_name {
        return reload_failure(
          selection.module_name,
          "duplicate state migration selection",
        )
      }
    }
    let archive = match
      save_context_state(current, selection.module_name, selection.globals) {
      VmStateSaved(value) => value
      VmStateSaveFailed(issue) => return ProgramFailed([issue], [])
    }
    archives.push({ module_name: selection.module_name, archive, })
    migration_targets.push(selection.module_name)
  }
  let candidate = make_vm_context(
    candidate_sources,
    self.limits,
    self.fallback_loader,
    copy_foreign_methods(self.foreign_methods),
    copy_foreign_classes(self.foreign_classes),
    self.should_cancel,
  )
  candidate.begin_operation()
  let mut last = Value::null()
  let migration_order = match
    reload_dependency_order(migration_targets, candidate_sources) {
    Ok(value) => value
    Err(message) => {
      finalize_foreign_instances(candidate)
      return reload_failure("", message)
    }
  }
  let replacement_order = match
    reload_dependency_order(targets, candidate_sources) {
    Ok(value) => value
    Err(message) => {
      finalize_foreign_instances(candidate)
      return reload_failure("", message)
    }
  }
  // Initialize state-owning modules first so their complete graphs can be
  // restored before dependent modules capture imported values.
  for target in migration_order {
    let source = match find_module_source(candidate_sources, target) {
      Some(value) => value
      None => {
        finalize_foreign_instances(candidate)
        return reload_failure(
          target, "state migration module has no retained or replacement source",
        )
      }
    }
    match execute_module_source(target, source, candidate) {
      ModuleExecutionFailed(issues) => {
        finalize_foreign_instances(candidate)
        return ProgramFailed(issues, copy_output(candidate.output))
      }
      ModuleExecuted(_, value) => last = value
    }
  }
  for saved in archives {
    let migrated = match migrate(saved) {
      Ok(value) => value
      Err(message) => {
        finalize_foreign_instances(candidate)
        return reload_failure(
          saved.module_name,
          "state migration failed: " + message,
        )
      }
    }
    match restore_context_state(candidate, migrated) {
      VmStateRestored(_) => ()
      VmStateRestoreFailed(issue) => {
        finalize_foreign_instances(candidate)
        return ProgramFailed([issue], copy_output(candidate.output))
      }
    }
  }
  // Then initialize the remaining replacements against the migrated state.
  for target in replacement_order {
    if migration_targets.contains(target) {
      continue
    }
    let source = match find_module_source(candidate_sources, target) {
      Some(value) => value
      None => {
        finalize_foreign_instances(candidate)
        return reload_failure(target, "replacement source is unavailable")
      }
    }
    match execute_module_source(target, source, candidate) {
      ModuleExecutionFailed(issues) => {
        finalize_foreign_instances(candidate)
        return ProgramFailed(issues, copy_output(candidate.output))
      }
      ModuleExecuted(_, value) => last = value
    }
  }
  for root in self.roots {
    if targets.contains(root) || migration_targets.contains(root) {
      continue
    }
    let source = match find_module_source(candidate_sources, root) {
      Some(value) => value
      None => {
        finalize_foreign_instances(candidate)
        return reload_failure(root, "root module has no retained source")
      }
    }
    match execute_module_source(root, source, candidate) {
      ModuleExecutionFailed(issues) => {
        finalize_foreign_instances(candidate)
        return ProgramFailed(issues, copy_output(candidate.output))
      }
      ModuleExecuted(_, value) => last = value
    }
  }
  match object_limit_issue(candidate, "") {
    Some(issue) => {
      finalize_foreign_instances(candidate)
      return ProgramFailed([issue], copy_output(candidate.output))
    }
    None => ()
  }
  finalize_foreign_instances(current)
  invalidate_vm_handles(self)
  self.generation = self.generation + 1
  self.sources = candidate_sources
  persistent_vm_contexts[self.context_identifier] = Some(candidate)
  ProgramCompleted(last, copy_output(candidate.output))
}

///|
/// Atomically replace multiple modules and preserve complete selected state
/// graphs without transforming the MWS1 archives.
pub fn Vm::reload_modules(
  self : Vm,
  replacements : Array[ModuleSource],
  preserve : Array[ReloadStateSelection],
) -> ProgramRunResult {
  self.reload_modules_with_migration(replacements, preserve, fn(saved) {
    Ok(saved.archive)
  })
}