///|
pub(all) enum ToastVariant {
  ToastDefault
  ToastDestructive
  ToastSuccess
} derive(Eq, Debug)

///|
pub(all) enum ToastPosition {
  ToastTopLeft
  ToastTopCenter
  ToastTopRight
  ToastBottomLeft
  ToastBottomCenter
  ToastBottomRight
} derive(Eq, Debug)

///|
struct Toast {
  id : String
  title : String
  description : String
  duration_ms : Int
  variant : ToastVariant
  action_label : String
  action : @minimoon.Cmd
  on_close : @minimoon.Cmd
  stamp : @minimoon.Node
}

///|
pub impl Eq for Toast with fn equal(a, b) {
  a.stamp == b.stamp
}

///|
pub fn toast_notice(
  id~ : String,
  title~ : String,
  description? : String = "",
  duration_ms? : Int = 5000,
  variant? : ToastVariant = ToastDefault,
  action_label? : String = "",
  action? : @minimoon.Cmd = @minimoon.none,
  on_close? : @minimoon.Cmd = @minimoon.none,
) -> Toast {
  guard id != "" && duration_ms >= 0 else {
    abort("toast requires an ID and nonnegative duration")
  }
  {
    id,
    title,
    description,
    duration_ms,
    variant,
    action_label,
    action,
    on_close,
    stamp: @minimoon.nothing(),
  }
}

///|
priv struct ToastEntry {
  notice : Toast
  remaining : Int
  swipe : (Int, Double, Double, Double)?
} derive(Eq)

///|
priv enum ToastMsg {
  Push(Toast)
  Dismiss(String)
  Activate(String)
  Tick
  Clear
  SwipeStart(String, @minimoon.TouchDetail)
  SwipeMove(String, @minimoon.TouchDetail)
  SwipeEnd(String, @minimoon.TouchDetail)
  SwipeCancel(String, @minimoon.TouchDetail)
  CancelGestures
}

///|
priv struct ToastQueue {
  entries : @minimoon.Val[Array[ToastEntry]]
  emit : @minimoon.Emit[ToastMsg]
}

///|
fn create_toast_queue(
  context : @minimoon.PageContext,
  visible : @minimoon.Val[Bool],
  limit : Int,
) -> ToastQueue {
  let initial : Array[ToastEntry] = []
  let (entries, emit) = @minimoon.create_state_with_input(
    input=visible,
    init=(_, _) => (initial, @minimoon.none),
    update=(entries, visible, message, _) => {
      match message {
        Activate(id) => {
          if !visible {
            return @minimoon.no_cmd(entries)
          }
          let active = entries.filter(entry => entry.notice.id == id)
          @minimoon.with_cmds(
            entries.filter(entry => entry.notice.id != id),
            active.map(entry => {
              @minimoon.batch([entry.notice.action, entry.notice.on_close])
            }),
          )
        }
        Push(notice) => {
          let next = entries.filter(entry => entry.notice.id != notice.id)
          let removed : Array[@minimoon.Cmd] = []
          while next.length() >= limit {
            let oldest = next.remove(0)
            removed.push(oldest.notice.on_close)
          }
          next.push({ notice, remaining: notice.duration_ms, swipe: None, })
          @minimoon.with_cmds(next, removed)
        }
        Dismiss(id) =>
          @minimoon.with_cmds(
            entries.filter(entry => entry.notice.id != id),
            entries
            .filter(entry => entry.notice.id == id)
            .map(entry => entry.notice.on_close),
          )
        Tick => {
          if !visible {
            return @minimoon.no_cmd(entries)
          }
          let next : Array[ToastEntry] = []
          let commands : Array[@minimoon.Cmd] = []
          for entry in entries {
            if entry.notice.duration_ms == 0 || entry.swipe is Some(_) {
              next.push(entry)
            } else if entry.remaining <= 250 {
              commands.push(entry.notice.on_close)
            } else {
              next.push({ ..entry, remaining: entry.remaining - 250, })
            }
          }
          @minimoon.with_cmds(next, commands)
        }
        Clear =>
          @minimoon.with_cmds([], entries.map(entry => entry.notice.on_close))
        SwipeStart(id, detail) =>
          @minimoon.no_cmd(
            entries.map(entry => {
              if entry.notice.id == id &&
                entry.swipe is None &&
                detail.touches.length() == 1 {
                let point = detail.touches[0]
                {
                  ..entry,
                  swipe: Some(
                    (point.identifier, point.client_x, point.client_y, 0.0),
                  ),
                }
              } else {
                entry
              }
            }),
          )
        SwipeMove(id, detail) =>
          @minimoon.no_cmd(
            entries.map(entry => {
              if entry.notice.id == id &&
                entry.swipe is Some((identifier, x, y, _)) {
                match
                  detail.touches
                  .filter(point => point.identifier == identifier)
                  .get(0) {
                  Some(point) =>
                    {
                      ..entry,
                      swipe: Some((identifier, x, y, point.client_x - x)),
                    }
                  None => entry
                }
              } else {
                entry
              }
            }),
          )
        SwipeEnd(id, detail) => {
          let next : Array[ToastEntry] = []
          let commands : Array[@minimoon.Cmd] = []
          for entry in entries {
            if entry.notice.id == id &&
              entry.swipe is Some((identifier, x, y, _)) {
              match
                detail.changed_touches
                .filter(point => point.identifier == identifier)
                .get(0) {
                Some(point) => {
                  let dx = (point.client_x - x).abs()
                  if dx >= 64.0 && dx > (point.client_y - y).abs() {
                    commands.push(entry.notice.on_close)
                  } else {
                    next.push({ ..entry, swipe: None, })
                  }
                }
                None => next.push(entry)
              }
            } else {
              next.push(entry)
            }
          }
          @minimoon.with_cmds(next, commands)
        }
        SwipeCancel(id, detail) =>
          @minimoon.no_cmd(
            entries.map(entry => {
              if entry.notice.id == id &&
                entry.swipe is Some((identifier, _, _, _)) &&
                detail.changed_touches.any(point => {
                  point.identifier == identifier
                }) {
                { ..entry, swipe: None, }
              } else {
                entry
              }
            }),
          )
        CancelGestures =>
          @minimoon.no_cmd(entries.map(entry => { ..entry, swipe: None, }))
      }
    },
    subscriptions=(entries, visible, emit) => {
      @minimoon.Sub::batch([
        context.on_unload(emit(Clear)),
        context.on_hide(emit(CancelGestures)),
        if visible && entries.any(entry => entry.notice.duration_ms > 0) {
          context.every(
            key="mmui-toast-clock",
            interval_ms=250,
            command=emit(Tick),
          )
        } else {
          @minimoon.Sub::none()
        },
      ])
    },
  )
  { entries, emit, }
}

///|
pub fn toast(context : UiContext, notice : Toast) -> @minimoon.Cmd {
  (context.toasts.emit)(Push(notice))
}

///|
pub fn dismiss_toast(context : UiContext, id : String) -> @minimoon.Cmd {
  (context.toasts.emit)(Dismiss(id))
}

///|
pub fn toaster(
  context : UiContext,
  position? : ToastPosition = ToastBottomRight,
  rich_colors? : Bool = false,
) -> @minimoon.Val[@minimoon.Node] {
  @minimoon.Val::view2(context.toasts.entries, context.visible, (
    entries,
    visible,
  ) => {
    if !visible || entries.is_empty() {
      return @minimoon.nothing()
    }
    @minimoon.layer(
      id="mmui-toast-layer",
      ui_container(
        "toaster",
        style=toast_position_style(position),
        entries.map(entry => {
          let notice = entry.notice
          @minimoon.touch_view(
            id=notice.id,
            class="mmui-toast",
            event_key="toast/" + notice.id + "/swipe",
            style=toast_variant_style(notice.variant, rich_colors) +
              (match entry.swipe {
                Some((_, _, _, delta)) =>
                  "transform:translateX(" + delta.to_string() + "px);"
                None => ""
              }),
            on_touch_start=@minimoon.Emit(detail => {
              (context.toasts.emit)(SwipeStart(notice.id, detail))
            }),
            on_touch_move=@minimoon.Emit(detail => {
              (context.toasts.emit)(SwipeMove(notice.id, detail))
            }),
            on_touch_end=@minimoon.Emit(detail => {
              (context.toasts.emit)(SwipeEnd(notice.id, detail))
            }),
            on_touch_cancel=@minimoon.Emit(detail => {
              (context.toasts.emit)(SwipeCancel(notice.id, detail))
            }),
            semantics=@minimoon.semantics(
              role=@minimoon.StatusRole,
              live="polite",
            ),
            [
              @minimoon.text(notice.title),
              @minimoon.text(notice.description),
              if notice.action_label == "" {
                @minimoon.nothing()
              } else {
                @minimoon.button(
                  event_key="toast/" + notice.id + "/action",
                  on_tap=(context.toasts.emit)(Activate(notice.id)),
                  notice.action_label,
                )
              },
              @minimoon.button(
                event_key="toast/" + notice.id + "/dismiss",
                on_tap=dismiss_toast(context, notice.id),
                "Dismiss",
              ),
            ],
          )
        }),
      ),
    )
  })
}

///|
pub fn sonner(
  context : UiContext,
  position? : ToastPosition = ToastBottomRight,
  rich_colors? : Bool = true,
) -> @minimoon.Val[@minimoon.Node] {
  toaster(context, position~, rich_colors~)
}