///|
struct EffectDescriptor {
  key : String
  kind : String
  label : String
} derive(Eq, Debug, ToJson)

///|
pub fn EffectDescriptor::key(self : EffectDescriptor) -> String {
  self.key
}

///|
pub fn EffectDescriptor::kind(self : EffectDescriptor) -> String {
  self.kind
}

///|
pub fn EffectDescriptor::label(self : EffectDescriptor) -> String {
  self.label
}

///|
pub(all) enum Effect[Msg] {
  NoEffect
  Batch(Array[Effect[Msg]])
  Send(Msg)
  Dispatch(((Msg) -> Unit) -> Unit)
  Run(EffectDescriptor, ((Msg) -> Unit) -> Unit)
  Task(EffectDescriptor, ((Msg) -> Unit) -> (() -> Unit)?)
}

///|
/// A host-visible command declaration whose action re-enters the Program as a
/// typed message. The runtime owns dispatch; applications only describe the
/// command and its message.
pub struct ProgramCommand[Msg] {
  command : ActionCommand
  message : Msg
}

///|
struct Program[Model, Msg] {
  init : () -> (Model, Effect[Msg])
  update : (Model, Msg) -> (Model, Effect[Msg])
  view : (Model, ViewEnvironment) -> View[Msg]
  subscriptions : (Model) -> Subscription[Msg]
  commands : (Model) -> Array[ProgramCommand[Msg]]
}

///|
pub fn[Msg] ProgramCommand::new(
  command~ : ActionCommand,
  message~ : Msg,
) -> ProgramCommand[Msg] {
  { command, message }
}

///|
pub fn[Msg] ProgramCommand::command(
  self : ProgramCommand[Msg],
) -> ActionCommand {
  self.command
}

///|
pub fn[Msg] ProgramCommand::message(self : ProgramCommand[Msg]) -> Msg {
  self.message
}

///|
pub fn[Msg, NextMsg] ProgramCommand::map(
  self : ProgramCommand[Msg],
  map : (Msg) -> NextMsg,
) -> ProgramCommand[NextMsg] {
  ProgramCommand::new(command=self.command, message=map(self.message))
}

///|
pub fn[Msg] Effect::none() -> Effect[Msg] {
  NoEffect
}

///|
pub fn[Msg] Effect::batch(effects : Array[Effect[Msg]]) -> Effect[Msg] {
  Batch(effects)
}

///|
pub fn[Msg] Effect::send(message : Msg) -> Effect[Msg] {
  Send(message)
}

///|
pub fn[Msg] Effect::dispatch(run : ((Msg) -> Unit) -> Unit) -> Effect[Msg] {
  Dispatch(run)
}

///|
pub fn[Msg] Effect::run(
  key~ : String,
  run~ : ((Msg) -> Unit) -> Unit,
  kind? : String = "custom",
  label? : String = "",
) -> Effect[Msg] {
  Run({ key, kind, label }, run)
}

///|
pub fn[Msg] Effect::task(
  key~ : String,
  start~ : ((Msg) -> Unit) -> (() -> Unit)?,
  kind? : String = "task",
  label? : String = "",
) -> Effect[Msg] {
  Task({ key, kind, label }, start)
}

///|
pub fn[Msg] Effect::service_task(
  key~ : String,
  start~ : ((Msg) -> Unit) -> (() -> Unit)?,
  label? : String = "",
) -> Effect[Msg] {
  Effect::task(key~, start~, kind="service", label~)
}

///|
pub fn[Msg] Effect::runtime_visit(
  self : Effect[Msg],
  send : (Msg) -> Unit,
  dispatch_effect : (((Msg) -> Unit) -> Unit) -> Unit,
  run_effect : (EffectDescriptor, ((Msg) -> Unit) -> Unit) -> Unit,
  start_task : (EffectDescriptor, ((Msg) -> Unit) -> (() -> Unit)?) -> Unit,
) -> Unit {
  match self {
    NoEffect => ()
    Batch(effects) =>
      for effect in effects {
        effect.runtime_visit(send, dispatch_effect, run_effect, start_task)
      }
    Send(message) => send(message)
    Dispatch(run) => dispatch_effect(run)
    Run(descriptor, run) => run_effect(descriptor, run)
    Task(descriptor, start) => start_task(descriptor, start)
  }
}

///|
pub fn[Msg, NextMsg] Effect::map(
  self : Effect[Msg],
  map : (Msg) -> NextMsg,
) -> Effect[NextMsg] {
  match self {
    NoEffect => NoEffect
    Batch(effects) => Batch(effects.map(effect => effect.map(map)))
    Send(message) => Send(map(message))
    Dispatch(run) =>
      Dispatch(dispatch => run(message => dispatch(map(message))))
    Run(descriptor, run) =>
      Run(descriptor, dispatch => run(message => dispatch(map(message))))
    Task(descriptor, start) =>
      Task(descriptor, dispatch => start(message => dispatch(map(message))))
  }
}

///|
pub fn[Model, Msg] Program::new(
  init~ : () -> (Model, Effect[Msg]),
  update~ : (Model, Msg) -> (Model, Effect[Msg]),
  view~ : (Model) -> View[Msg],
  subscriptions? : (Model) -> Subscription[Msg] = _model => Subscription::none(),
) -> Program[Model, Msg] {
  {
    init,
    update,
    view: (model, _env) => view(model),
    subscriptions,
    commands: _model => [],
  }
}

///|
pub fn[Model, Msg] Program::new_with_environment(
  init~ : () -> (Model, Effect[Msg]),
  update~ : (Model, Msg) -> (Model, Effect[Msg]),
  view~ : (Model, ViewEnvironment) -> View[Msg],
  subscriptions? : (Model) -> Subscription[Msg] = _model => Subscription::none(),
) -> Program[Model, Msg] {
  { init, update, view, subscriptions, commands: _model => [] }
}

///|
pub fn[Model, Msg] Program::with_commands(
  self : Program[Model, Msg],
  commands~ : (Model) -> Array[ProgramCommand[Msg]],
) -> Program[Model, Msg] {
  { ..self, commands, }
}

///|
pub fn[Model, Msg] Program::simple(
  init~ : Model,
  update~ : (Model, Msg) -> Model,
  view~ : (Model) -> View[Msg],
  subscriptions? : (Model) -> Subscription[Msg] = _model => Subscription::none(),
) -> Program[Model, Msg] {
  Program::new(
    init=() => (init, Effect::none()),
    update=(model, message) => (update(model, message), Effect::none()),
    view~,
    subscriptions~,
  )
}

///|
pub fn[Model, Msg] Program::simple_with_environment(
  init~ : Model,
  update~ : (Model, Msg) -> Model,
  view~ : (Model, ViewEnvironment) -> View[Msg],
  subscriptions? : (Model) -> Subscription[Msg] = _model => Subscription::none(),
) -> Program[Model, Msg] {
  Program::new_with_environment(
    init=() => (init, Effect::none()),
    update=(model, message) => (update(model, message), Effect::none()),
    view~,
    subscriptions~,
  )
}

///|
pub fn[Model, Msg] Program::runtime_init(
  self : Program[Model, Msg],
) -> (Model, Effect[Msg]) {
  (self.init)()
}

///|
pub fn[Model, Msg] Program::runtime_update(
  self : Program[Model, Msg],
  model : Model,
  message : Msg,
) -> (Model, Effect[Msg]) {
  (self.update)(model, message)
}

///|
pub fn[Model, Msg] Program::runtime_view(
  self : Program[Model, Msg],
  model : Model,
  environment : ViewEnvironment,
) -> View[Msg] {
  (self.view)(model, environment)
}

///|
pub fn[Model, Msg] Program::runtime_subscriptions(
  self : Program[Model, Msg],
  model : Model,
) -> Subscription[Msg] {
  (self.subscriptions)(model)
}

///|
pub fn[Model, Msg] Program::runtime_commands(
  self : Program[Model, Msg],
  model : Model,
) -> Array[ProgramCommand[Msg]] {
  (self.commands)(model)
}