///|
priv struct Namespace[V] {
  builtins : Map[String, V]
  current : Map[String, V]
  undo_stack : Array[Map[String, V?]]
}

///|
fn[V] Namespace::make(
  builtins : Map[String, V],
  initial : Map[String, V],
) -> Namespace[V] {
  { builtins, current: initial, undo_stack: [] }
}

///|
fn[V] Namespace::begin_group(self : Namespace[V]) -> Unit {
  self.undo_stack.push(Map([]))
}

///|
fn[V] Namespace::restore_group(
  self : Namespace[V],
  changes : Map[String, V?],
) -> Unit {
  for key, old_value in changes {
    match old_value {
      Some(value) => self.current[key] = value
      None => self.current.remove(key)
    }
  }
}

///|
fn[V] Namespace::end_group(self : Namespace[V]) -> Unit raise ParseFailure {
  guard self.undo_stack.pop() is Some(changes) else {
    raise InternalInvariant(
      message="Unbalanced namespace destruction: attempt to pop global namespace; please report this as a bug",
    )
  }
  self.restore_group(changes)
}

///|
fn[V] Namespace::end_groups(self : Namespace[V]) -> Unit {
  while !self.undo_stack.is_empty() {
    if self.undo_stack.pop() is Some(changes) {
      self.restore_group(changes)
    }
  }
}

///|
fn[V] Namespace::has(self : Namespace[V], key : String) -> Bool {
  self.current.contains(key) || self.builtins.contains(key)
}

///|
fn[V] Namespace::get(self : Namespace[V], key : String) -> V? {
  match self.current.get(key) {
    Some(value) => Some(value)
    None => self.builtins.get(key)
  }
}

///|
fn[V] Namespace::get_current(self : Namespace[V], key : String) -> V? {
  self.current.get(key)
}

///|
fn[V] Namespace::get_builtin(self : Namespace[V], key : String) -> V? {
  self.builtins.get(key)
}

///|
fn[V] Namespace::get_user_entries(self : Namespace[V]) -> Map[String, V] {
  let result : Map[String, V] = Map([])
  for key, value in self.current {
    if !self.builtins.contains(key) {
      result[key] = value
    }
  }
  result
}

///|
#warnings("-unused_value")
fn[V] Namespace::set(
  self : Namespace[V],
  key : String,
  value : V?,
  global? : Bool = false,
) -> Unit {
  if global {
    for changes in self.undo_stack {
      changes.remove(key)
    }
    if self.undo_stack.last() is Some(changes) {
      changes[key] = value
    }
  } else if self.undo_stack.last() is Some(changes) && !changes.contains(key) {
    changes[key] = self.current.get(key)
  }
  match value {
    Some(v) => self.current[key] = v
    None => self.current.remove(key)
  }
}