///|
using @runtime {trait IsCell}

///|
priv struct TypedCell[Model, Msg] {
  model : Ref[Model]
  dispatcher : Dispatcher[Msg]
  dispatch : (Msg) -> Cmd
  update : (Dispatch[Msg], Msg, Model) -> (Cmd, Model)
  view : (Dispatch[Msg], Model) -> @html.Html
  flags : @runtime.Flags
}

///|
/// Message dispatcher type used by a `Cell`.
///
/// Calling `dispatch(msg)` returns a `Cmd` that enqueues `msg` into the cell's
/// update loop.
pub type Dispatch[Msg] = (Msg) -> Cmd

///|
impl[Model, Msg] IsCell for TypedCell[Model, Msg] with step(self, scheduler) {
  if self.dispatcher.inbox.pop() is Some(msg) {
    let (cmd, model) = (self.update)(self.dispatch, msg, self.model.val)
    self.model.val = model
    scheduler.add(cmd)
    self.flags.mark_dirty()
  } else {
    // violate the invariant
  }
}

///|
impl[Model, Msg] IsCell for TypedCell[Model, Msg] with view(self) {
  (self.view)(self.dispatch, self.model.val).0
}

///|
impl[Model, Msg] IsCell for TypedCell[Model, Msg] with flags(self) {
  self.flags
}

///|
priv struct Dispatcher[Msg] {
  id : @runtime.Id
  inbox : @queue.Queue[Msg]
}

///|
fn[Msg] Dispatcher::message(self : Self[Msg], msg : Msg) -> Cmd {
  // carry the drain (which own model and update) in message will cause memeory leak in RC
  @runtime.message(self.id, () => self.inbox.push(msg))
}

///|
/// A cell that encapsulates model, update, and view.
struct Cell(&IsCell)

///|
/// Render this cell as `Html`.
pub fn Cell::view(self : Self) -> Html {
  Html::from_vnode(@runtime.VNode::slot(self.0))
}

///|
/// Create a stateful cell with full `model`/`update`/`view` functionality.
///
/// Type parameters:
///
/// - `Model`: a custom type you need to defiend, represent the state of this cell.
/// - `Msg`: a custom `enum` you need to defined, represent the events of this cell. 
///
/// Parameters:
///
/// - `model : Model`:
///    initial `Model` value of the cell.
/// 
/// - `update : (Dispatch[Msg], Msg,Model) -> (Cmd,Model)`:
///    Describe how to compute new `Model` from `Msg` and old `Model`. 
///    This callback also receives a `Dispatch[Msg]`, which converts a `Msg`
///    into a `Cmd`.
/// 
/// - `view : (Dispatch[Msg], Model) -> Html`:
///    Describe how to compute `Html` from `Model`.
///    This callback also receives a `Dispatch[Msg]`, which converts a `Msg`
///    into a `Cmd`.
///
/// ## The update loop
///
/// ```text
/// ┌─────────────────┐
/// │                 ▼
/// │            ┌──────────┐
/// │            │   user   │
/// │            └────┬─────┘
/// │                 │ msg,model
/// │                 ▼
/// │           ┌────────────┐   msg,model
/// │           │  update()  │◄────────────┐
/// │           └──┬───────┬─┘             │
/// │    none,model│       │               │
/// │              ▼       │               │
/// │         ┌────────┐   │cmd,model      │
/// │         │ view() │   │               │
/// │         └────┬───┘   │               │
/// │              │       │               │
/// │          html│       ▼               │
/// │              │   ┌─────────┐         │
/// └──────────────┘   │ runtime │─────────┘
///                    └─────────┘
/// ```
/// 
/// This update loop is a bit more complex than `simple_cell`, but follows the
/// same `model -> update -> view` flow.
///
/// The difference is that `update` also returns a command representing a managed
/// side effect. That side effect is executed by the runtime and may produce
/// another message.
///
pub fn[Model, Msg] cell(
  model~ : Model,
  update~ : (Dispatch[Msg], Msg, Model) -> (Cmd, Model),
  view~ : (Dispatch[Msg], Model) -> Html,
) -> Cell {
  let flags = @runtime.Flags::new()
  let dispatcher = { id: flags.id(), inbox: @queue.new() }
  let dispatch = m => dispatcher.message(m)
  let model = @ref.new(model)
  Cell({ view, update, dispatcher, dispatch, model, flags })
}

///|
/// Create a cell and also return its `Dispatch`.
///
/// This is useful when messages need to be sent from outside.
/// 
/// Type parameters:
///
/// - `Model`: a custom type you need to defiend, represent the state of this cell.
/// - `Msg`: a custom `enum` you need to defined, represent the events of this cell. 
///
/// Parameters:
///
/// - `model : Model`:
///    initial `Model` value of the cell.
/// 
/// - `update : (Dispatch[Msg], Msg,Model) -> (Cmd,Model)`:
///    Describe how to compute new `Model` from `Msg` and old `Model`. 
///    This callback also receives a `Dispatch[Msg]`, which converts a `Msg`
///    into a `Cmd`.
/// 
/// - `view : (Dispatch[Msg], Model) -> Html`:
///    Describe how to compute `Html` from `Model`.
///    This callback also receives a `Dispatch[Msg]`, which converts a `Msg`
///    into a `Cmd`.
///
/// ## The update loop
///
/// ```text
/// ┌─────────────────┐
/// │                 ▼
/// │            ┌──────────┐
/// │            │   user   │
/// │            └────┬─────┘
/// │                 │ msg,model
/// │                 ▼
/// │           ┌────────────┐   msg,model
/// │           │  update()  │◄────────────┐
/// │           └──┬───────┬─┘             │
/// │    none,model│       │               │
/// │              ▼       │               │
/// │         ┌────────┐   │cmd,model      │
/// │         │ view() │   │               │
/// │         └────┬───┘   │               │
/// │              │       │               │
/// │          html│       ▼               │
/// │              │   ┌─────────┐         │
/// └──────────────┘   │ runtime │─────────┘
///                    └─────────┘
/// ```
/// 
/// This update loop is a bit more complex than `simple_cell`, but follows the
/// same `model -> update -> view` flow.
///
/// The difference is that `update` also returns a command representing a managed
/// side effect. That side effect is executed by the runtime and may produce
/// another message.
///
pub fn[Model, Msg] cell_with_dispatch(
  model~ : Model,
  update~ : (Dispatch[Msg], Msg, Model) -> (Cmd, Model),
  view~ : (Dispatch[Msg], Model) -> Html,
) -> (Dispatch[Msg], Cell) {
  let flags = @runtime.Flags::new()
  let dispatcher = { id: flags.id(), inbox: @queue.new() }
  let dispatch = m => dispatcher.message(m)
  let model = @ref.new(model)
  (dispatch, Cell({ view, update, dispatcher, dispatch, model, flags }))
}

///|
/// Create a cell with simplified `model`/`update`/`view`.
/// 
/// Type parameters:
/// 
/// - `Model`: a custom type you need to defiend, represent the state of this cell.
/// - `Msg`: a custom `enum` you need to defined, represent the events of this cell. 
/// 
/// Parameters:
/// 
/// - `model : Model`: initial Model value of the cell.
/// - `update : (Msg, Model) -> Model`: describe how to compute new Model from Msg and old Model.
/// - `view : (Dispatch[Msg], Model) -> Html`: describes how to render Html from Model.
///   This callback also receives a dispatch argument, which converts a Msg
///   into a Cmd.
/// 
/// ## The update loop
///
/// ```text
///    ┌──────┐
///    │ user │◀─────────┐
///    └──────┘          │
///        │ msg, model  │
///        ▼             │
///  ┌──────────┐        │
///  │ update() │        │ html
///  └──────────┘        │
///        │ model       │
///        ▼             │
///   ┌────────┐         │
///   │ view() │─────────┘
///   └────────┘
/// ```
/// 
/// At startup, the initial `model` is rendered as `Html` by `view`. The rendered 
/// HTML can include message-producing command, for example:
/// 
/// ```moonbit nocheck
/// button(on_click=dispatch(MyMsg), "click me")
/// ```
///
/// When the user clicks the button, `MyMsg` is sent to `update` with the current
/// model. `update` then computes the next model from that message, and the new
/// model is rendered again by `view`.
/// 
/// ## Example 
/// 
/// ```moonbit check
/// test "counter" {
///   struct Model {
///     count : Int
///   }
///   enum Msg {
///     Click
///   }
///   let app = @rabbita.simple_cell(
///     model={ count: 0 },
///     update=(msg, model) => {
///       match msg {
///         Click => { count: model.count + 1 }
///       }
///     },
///     view=(dispatch, model) => {
///       div([
///         h1("You clicked \{model.count} times."),
///         button(on_click=dispatch(Click), "+"),
///       ])
///     },
///   )
///   ignore(app) // use `new(app).mount("id")` in client
/// }
/// ```
pub fn[Model, Msg] simple_cell(
  model~ : Model,
  update~ : (Msg, Model) -> Model,
  view~ : (Dispatch[Msg], Model) -> Html,
) -> Cell {
  cell(model~, update=(_, msg, model) => (none, update(msg, model)), view~)
}

///|
pub fn[Model, Msg] simple_cell_with_dispatch(
  model~ : Model,
  update~ : (Msg, Model) -> Model,
  view~ : (Dispatch[Msg], Model) -> Html,
) -> (Dispatch[Msg], Cell) {
  let flags = @runtime.Flags::new()
  let dispatcher = { id: flags.id(), inbox: @queue.new() }
  let dispatch = m => dispatcher.message(m)
  let model = @ref.new(model)
  (
    dispatch,
    Cell({
      view,
      update: (_, msg, model) => (none, update(msg, model)),
      dispatcher,
      dispatch,
      model,
      flags,
    }),
  )
}

///|
/// Create a static cell that always renders the same `Html`.
///
/// Rather than use `static_cell`, it's recommend to wrap a function that return html directly:
/// 
/// ```moonbit nocheck
/// fn button(text : String, on_click~ : Cmd) -> Html {
///   @html.button(style=["..."], on_click~, text)
/// }
/// ```
pub fn static_cell(html : Html) -> Cell {
  cell(model=(), update=fn(_, _ : Unit, _ : Unit) { (none, ()) }, view=fn(
    _,
    _,
  ) {
    html
  })
}