///|
/// Factories construct fresh game state. They do not perform scene transitions.
pub struct InitialStates[T] {
  default_factory : (Int, Int) -> T
  named : Array[(String, (Int, Int) -> T)]
}

///|
pub suberror InitialStateError {
  InitialStateError(String)
}

///|
fn valid_initial_state_name(name : String) -> Bool {
  if name.is_empty() || name.length() > 128 {
    return false
  }
  for ch in name {
    let n = ch.to_int()
    if !((n >= 65 && n <= 90) ||
      (n >= 97 && n <= 122) ||
      (n >= 48 && n <= 57) ||
      ch == '_' ||
      ch == '-') {
      return false
    }
  }
  true
}

///|
pub fn[T] InitialStates::new(
  default~ : (Int, Int) -> T,
  named? : Array[(String, (Int, Int) -> T)] = [],
) -> InitialStates[T] raise InitialStateError {
  let names : Array[String] = []
  if named.length() > 32 {
    raise InitialStateError("Too many initial states")
  }
  for (name, _) in named {
    if !valid_initial_state_name(name) || names.contains(name) {
      raise InitialStateError("Invalid or duplicate initial state: " + name)
    }
    names.push(name)
  }
  { default_factory: default, named: named.copy(), }
}

///|
pub fn[T] InitialStates::names(self : InitialStates[T]) -> Array[String] {
  self.named.map(entry => entry.0)
}

///|
/// No fallback for unknown names: callers must know which state they are testing.
pub fn[T] InitialStates::create(
  self : InitialStates[T],
  name : String?,
  width~ : Int,
  height~ : Int,
) -> T raise InitialStateError {
  if width < 1 || height < 1 {
    raise InitialStateError("Invalid initial viewport")
  }
  match name {
    None => (self.default_factory)(width, height)
    Some(name) => {
      for (key, make) in self.named {
        if key == name {
          return make(width, height)
        }
      }
      raise InitialStateError("Unknown initial state: " + name)
    }
  }
}

///|
let applied_initial_state : Ref[String?] = Ref(None)

///|
/// Resolve the host request before constructing state. Works with EngineGame,
/// SceneGame and custom loops because the result is the game's own T.
pub fn[T] InitialStates::create_for_capture(
  self : InitialStates[T],
  width~ : Int,
  height~ : Int,
) -> T {
  let (width, height) = capture_viewport(width, height)
  let name = requested_initial_state()
  let state = self.create(name, width~, height~) catch {
    InitialStateError(message) => {
      record_initial_state_failure(message)
      abort(message)
    }
  }
  applied_initial_state.val = name
  state
}