///|
priv enum OverlayRequest {
  Request(Bool, OverlayCloseReason)
  Apply(Int, Bool, OverlayCloseReason)
  Hide
  InputChanged
  Toggle
  Act(@minimoon.Cmd, Bool, OverlayCloseReason)
}

///|
pub fn[Input : Eq] overlay_provider_controlled_with_input(
  context : UiContext,
  id~ : String,
  input~ : @minimoon.Val[Input],
  open~ : @minimoon.Val[Bool],
  on_open_change~ : @minimoon.Emit[Bool],
  render~ : (OverlayScope, Input) -> @minimoon.Node,
  kind? : OverlayKind = DialogSurface,
  disabled? : Bool = false,
  modal? : Bool,
  close_on_overlay? : Bool = true,
  force_mount? : Bool = false,
  placement? : @headless.Placement = @headless.BottomStart,
  side_offset? : Double = 8.0,
  align_offset? : Double = 0.0,
  delay_ms? : Int = 0,
  close_delay_ms? : Int = 0,
  on_close? : @minimoon.Emit[OverlayCloseReason],
) -> @minimoon.Val[@minimoon.Node] {
  let floating = kind is PopoverSurface ||
    kind is PreviewSurface ||
    kind is TooltipSurface ||
    kind is ContextSurface
  let state = @minimoon.Val::map2(open, context.visible, (open, visible) => {
    (open, visible)
  })
  let (_, emit) = @minimoon.create_state_with_input(
    input=state,
    init=(_, _) => @minimoon.no_cmd(0),
    update=(token, state, message : OverlayRequest, emit) => {
      let (open, visible) = state
      match message {
        Act(command, close, reason) =>
          (
            token,
            if open && visible {
              @minimoon.batch([
                command,
                if close {
                  emit(Request(false, reason))
                } else {
                  @minimoon.none
                },
              ])
            } else {
              @minimoon.none
            },
          )
        Toggle => (token, emit(Request(!open, Programmatic)))
        InputChanged => @minimoon.no_cmd(token + 1)
        Request(next, reason) => {
          if disabled && next {
            return @minimoon.no_cmd(token)
          }
          let delay = if next { delay_ms } else { close_delay_ms }
          (
            token + 1,
            if delay <= 0 {
              emit(Apply(token + 1, next, reason))
            } else {
              @minimoon.delay(emit(Apply(token + 1, next, reason)), delay)
            },
          )
        }
        Apply(expected, next, reason) => {
          if token != expected || !visible || (next && disabled) {
            return @minimoon.no_cmd(token)
          }
          (
            token,
            @minimoon.batch([
              on_open_change(next),
              if open && !next {
                on_close.map(emit => emit(reason)).unwrap_or(@minimoon.none)
              } else {
                @minimoon.none
              },
            ]),
          )
        }
        Hide =>
          (
            token + 1,
            if floating && open {
              @minimoon.batch([
                on_open_change(false),
                on_close.map(emit => emit(PageHidden)).unwrap_or(@minimoon.none),
              ])
            } else {
              @minimoon.none
            },
          )
      }
    },
    subscriptions=(_, _, emit) => context.page.on_hide(emit(Hide)),
  )
  let watcher = open.switch_by(
    (_, _) => {
      let (_, _) = @minimoon.create_state_with_init(
        init=_ => ((), emit(InputChanged)),
        update=(_, _ : Unit, _) => @minimoon.no_cmd(()),
      )
      @minimoon.Val::constant(@minimoon.nothing())
    },
    by=open => if open { "open" } else { "closed" },
  )
  let bounds = if floating {
    floating_bounds(context, id, open, placement, side_offset~, align_offset~)
  } else {
    @minimoon.Val::constant(None)
  }
  let rendered = @minimoon.Val::view4(open, input, bounds, context.visible, (
    open,
    input,
    bounds,
    visible,
  ) => {
    render(
      OverlayScope::{
        id,
        open,
        visible,
        disabled,
        modal: modal.unwrap_or(!floating),
        force_mount,
        close_on_overlay,
        kind,
        bounds,
        toggle: emit(Toggle),
        act: emit.map(value => {
          let (command, close, reason) = value
          Act(command, close, reason)
        }),
        change: emit.map(value => {
          let (open, reason) = value
          Request(open, reason)
        }),
      },
      input,
    )
  })
  @minimoon.Val::view2(rendered, watcher, (rendered, watcher) => {
    @minimoon.fragment([rendered, watcher])
  })
}

///|
pub fn overlay_provider_controlled(
  context : UiContext,
  id~ : String,
  open~ : @minimoon.Val[Bool],
  on_open_change~ : @minimoon.Emit[Bool],
  render~ : (OverlayScope) -> @minimoon.Node,
  kind? : OverlayKind = DialogSurface,
  disabled? : Bool = false,
  modal? : Bool,
  close_on_overlay? : Bool = true,
  force_mount? : Bool = false,
  placement? : @headless.Placement = @headless.BottomStart,
  side_offset? : Double = 8.0,
  align_offset? : Double = 0.0,
  delay_ms? : Int = 0,
  close_delay_ms? : Int = 0,
  on_close? : @minimoon.Emit[OverlayCloseReason],
) -> @minimoon.Val[@minimoon.Node] {
  overlay_provider_controlled_with_input(
    context,
    id~,
    input=@minimoon.Val::constant(()),
    open~,
    on_open_change~,
    render=(scope, _) => render(scope),
    kind~,
    disabled~,
    modal?,
    close_on_overlay~,
    force_mount~,
    placement~,
    side_offset~,
    align_offset~,
    delay_ms~,
    close_delay_ms~,
    on_close?,
  )
}

///|
pub fn overlay_provider(
  context : UiContext,
  id~ : String,
  render~ : (OverlayScope) -> @minimoon.Node,
  initially_open? : Bool = false,
  kind? : OverlayKind = DialogSurface,
  disabled? : Bool = false,
  modal? : Bool,
  close_on_overlay? : Bool = true,
  force_mount? : Bool = false,
  placement? : @headless.Placement = @headless.BottomStart,
  side_offset? : Double = 8.0,
  align_offset? : Double = 0.0,
  delay_ms? : Int = 0,
  close_delay_ms? : Int = 0,
  on_close? : @minimoon.Emit[OverlayCloseReason],
  on_open_change? : @minimoon.Emit[Bool],
) -> @minimoon.Val[@minimoon.Node] {
  let (open, emit) = @minimoon.create_variable(initially_open)
  overlay_provider_controlled(
    context,
    id~,
    open~,
    on_open_change=@minimoon.Emit(value => {
      @minimoon.batch([
        emit(_ => value),
        on_open_change.map(emit => emit(value)).unwrap_or(@minimoon.none),
      ])
    }),
    render~,
    kind~,
    disabled~,
    modal?,
    close_on_overlay~,
    force_mount~,
    placement~,
    side_offset~,
    align_offset~,
    delay_ms~,
    close_delay_ms~,
    on_close?,
  )
}