///|
struct Update[Model, Message]((Model, Message, (Message) -> Unit) -> Model)

///|
pub fn[Model, Message] update(
  f : (Model, Message) -> Model,
) -> Update[Model, Message] {
  Update((model, message, _) => f(model, message))
}

///|
pub fn[Model, Message, Effect] effect(
  update : (Model, Message) -> (Model, Effect),
  handle : (Effect, (Message) -> Unit) -> Unit,
) -> Update[Model, Message] {
  Update((model, message, dispatch) => {
    let (model, effect) = update(model, message)
    handle(effect, dispatch)
    model
  })
}

///|
priv struct State[Model, Message] {
  app : @gtk.Application
  mut model : Model
  render : (Model) -> VWidget[Message]
  update : (Model, Message, (Message) -> Unit) -> Model
  subscribe : @sub.Subscribe[Model, Message]?
  mut widget : IWidget?
  mut dirty : Bool
  mut pending : Bool
  mut dispatching : Bool
  queue : @deque.Deque[Message]
}

///|
fn[Model, Message] State::apply(
  self : State[Model, Message],
  message : Message,
  dispatch : (Message) -> Unit,
) -> Unit {
  self.model = (self.update)(self.model, message, dispatch)
  if self.subscribe is Some(subscribe) {
    subscribe(self.model, dispatch)
  }
}

///|
fn[Model, Message] State::dispatch(
  self : State[Model, Message],
  message : Message,
) -> Unit {
  if self.dispatching {
    self.queue.push_back(message)
    return
  }
  self.dispatching = true
  let dispatch = msg => self.dispatch(msg)
  self.apply(message, dispatch)
  while self.queue.pop_front() is Some(msg) {
    self.apply(msg, dispatch)
  }
  self.dispatching = false
  self.dirty = true
  self.schedule()
}

///|
fn[Model, Message] State::render(self : State[Model, Message]) -> Unit {
  if !self.dirty {
    return
  }
  self.dirty = false
  let dispatch = message => self.dispatch(message)
  let new_view = (self.render)(self.model)
  if self.widget is Some(old_widget) {
    self.widget = Some(
      reconcile_widget(self.app, dispatch, old_widget, new_view),
    )
  } else {
    self.widget = Some(new_view.create(self.app, dispatch))
  }
}

///|
fn[Model, Message] State::schedule(self : State[Model, Message]) -> Unit {
  if self.pending {
    return
  }
  self.pending = true
  @glib.idle_add(() => {
    self.pending = false
    self.render()
    false
  })
  |> ignore()
}

///|
fn[Model, Message] start_app(
  app : @gtk.Application,
  model~ : Model,
  init? : Message,
  update~ : (Model, Message, (Message) -> Unit) -> Model,
  subscribe? : @sub.Subscribe[Model, Message],
  render~ : (Model) -> VWidget[Message],
) -> Unit {
  let state : State[Model, Message] = {
    app,
    render,
    update,
    subscribe,
    model,
    widget: None,
    dirty: true,
    pending: false,
    dispatching: false,
    queue: @deque.Deque([]),
  }
  app.connect_activate(_ => {
    let dispatch = message => state.dispatch(message)
    if init is Some(message) {
      dispatch(message)
    }
    if subscribe is Some(subscribe) {
      subscribe(state.model, dispatch)
    }
    state.render()
  })
  |> ignore()
  @sys.exit(app.run())
}

///|
pub fn[Model, Message] app(
  id? : String = "com.giraffe.app",
  model~ : Model,
  init? : Message,
  update~ : Update[Model, Message],
  render~ : (Model) -> VWidget[Message],
  subscribe? : @sub.Subscribe[Model, Message],
) -> Unit {
  let app = @gtk.Application::new(Some(id), @gio.ApplicationFlags([]))
  start_app(app, model~, init?, update=update.0, render~, subscribe?)
}

///|
pub fn[Model, Message] adw_app(
  id? : String = "com.giraffe.app",
  model~ : Model,
  init? : Message,
  update~ : Update[Model, Message],
  render~ : (Model) -> VWidget[Message],
  subscribe? : @sub.Subscribe[Model, Message],
) -> Unit {
  let adw_application = @adw.Application::new(
    Some(id),
    @gio.ApplicationFlags([]),
  )
  start_app(
    adw_application.as_gtk_application(),
    model~,
    init?,
    update=update.0,
    render~,
    subscribe?,
  )
}