///|
/// An optional application definition. Constructing it does not run effects.
struct App[Deps] {
  build : (AppContext) -> Deps
  capabilities : Array[Capability]
}

///|
struct AppContext(@val.Application)

///|
/// A read-only application value. Bind it to a page before composing a view.
struct Shared[T](@val.Shared[T])

///|
struct AppRuntime[Deps] {
  driver : AppDriver
  dependencies : Deps
}

///|
pub fn[Deps] App::capabilities(self : App[Deps]) -> Array[Capability] {
  self.capabilities.copy()
}

///|
pub fn[Deps] app(
  build~ : (AppContext) -> Deps,
  capabilities? : Array[Capability] = [],
) -> App[Deps] {
  for capability in capabilities {
    guard capability is (Request | Login | GetStorage | SetStorage) else {
      abort("unsupported application capability: " + capability.name())
    }
  }
  { build, capabilities: capabilities.copy(), }
}

///|
pub fn[Deps] App::create_runtime(self : App[Deps]) -> AppRuntime[Deps] {
  let owner = @val.Application::new()
  let driver = AppDriver::new(owner, self.capabilities)
  let dependencies = owner.build(() => (self.build)(AppContext(owner)))
  { driver, dependencies, }
}

///|
/// Inspect initial page contracts without launching effects or subscriptions.
pub fn[Deps, A] App::preview(self : App[Deps], inspect : (Deps) -> A) -> A {
  let runtime = self.create_runtime()
  defer ignore(runtime.dispose())
  runtime.driver.owner.with_pages(() => inspect(runtime.deps()))
}

///|
pub fn[Deps] AppRuntime::deps(self : AppRuntime[Deps]) -> Deps {
  self.dependencies
}

///|
pub fn[Deps] AppRuntime::create_page(
  self : AppRuntime[Deps],
  page : Page,
  input? : Map[String, String] = Map([]),
  layout? : PageLayout = PageLayout::unavailable(),
) -> Result[PageRuntime, DecodeError] {
  self.driver.owner.with_pages(() => page.create_runtime(input~, layout~))
}

///|
pub fn[Model : Eq, Msg] AppContext::create_state_with_init(
  self : AppContext,
  init~ : (Emit[Msg]) -> (Model, Cmd),
  update~ : (Model, Msg, Emit[Msg]) -> (Model, Cmd),
  subscriptions? : (Model, Emit[Msg]) -> Sub,
) -> (Shared[Model], Emit[Msg]) {
  let (value, emit) = self.0.create_state_with_init(
    emit => {
      let (model, cmd) = init(wrap_emit(emit))
      (model, cmd.0)
    },
    (model, msg, emit) => {
      let (next, cmd) = update(model, msg, wrap_emit(emit))
      (next, cmd.0)
    },
    subscriptions.map(build => (model, emit) => build(model, wrap_emit(emit)).0),
  )
  (Shared(value), wrap_emit(emit))
}

///|
pub fn[Model : Eq, Msg] AppContext::create_state(
  self : AppContext,
  initial : Model,
  update~ : (Model, Msg, Emit[Msg]) -> (Model, Cmd),
  subscriptions? : (Model, Emit[Msg]) -> Sub,
) -> (Shared[Model], Emit[Msg]) {
  self.create_state_with_init(
    init=_ => (initial, none),
    update~,
    subscriptions?,
  )
}

///|
pub fn[Model : Eq, Msg] AppContext::create_pure_state(
  self : AppContext,
  initial : Model,
  update~ : (Model, Msg) -> Model,
) -> (Shared[Model], Emit[Msg]) {
  self.create_state(initial, update=(model, msg, _) => {
    (update(model, msg), none)
  })
}

///|
pub fn[T : Eq] PageContext::bind(
  self : PageContext,
  shared : Shared[T],
) -> Val[T] {
  self.select(shared, value => value)
}

///|
pub fn[T : Eq, U : Eq] PageContext::select(
  self : PageContext,
  shared : Shared[T],
  select : (T) -> U,
) -> Val[U] {
  Val(self.graph.select_shared(shared.0, select))
}

///|
pub(all) enum AppLifecycleHook {
  Launch
  Show
  Hide
} derive(Eq)

///|
fn AppLifecycleHook::host_name(self : AppLifecycleHook) -> String {
  match self {
    Launch => "onLaunch"
    Show => "onShow"
    Hide => "onHide"
  }
}

///|
pub fn AppContext::lifecycle(
  self : AppContext,
  hook : AppLifecycleHook,
  decode : (Json) -> Result[Cmd, DecodeError],
) -> Sub {
  let _ = self
  Sub(
    @val.lifecycle(hook.host_name(), payload => {
      match decode(payload) {
        Ok(command) => Ok(command.0)
        Err(error) => Err(@val.decode_error(error.message()))
      }
    }),
  )
}

///|
pub fn AppContext::on_launch(self : AppContext, command : Cmd) -> Sub {
  self.lifecycle(Launch, _ => Ok(command))
}

///|
pub fn AppContext::on_show(self : AppContext, command : Cmd) -> Sub {
  self.lifecycle(Show, _ => Ok(command))
}

///|
pub fn AppContext::on_hide(self : AppContext, command : Cmd) -> Sub {
  self.lifecycle(Hide, _ => Ok(command))
}

///|
pub fn AppContext::every(
  self : AppContext,
  key~ : String,
  interval_ms~ : Int,
  command~ : Cmd,
) -> Sub {
  let _ = self
  Sub(@val.every(key, interval_ms, command.0))
}