namespace Lumi.Companion.Overlay; public enum OverlayItemState { Queued, Entering, Visible, Exiting, Removed } public sealed class OverlayQueueItem(string id, string kind, object payload, DateTimeOffset queuedAt) { public string Id { get; } = id; public string Kind { get; } = kind; public object Payload { get; } = payload; public DateTimeOffset QueuedAt { get; } = queuedAt; public DateTimeOffset StateAt { get; internal set; } = queuedAt; public OverlayItemState State { get; internal set; } = OverlayItemState.Queued; } public sealed class OverlayLifecycleQueue(Func settings, Func? now = null) { private readonly List _items = []; private readonly Func _now = now ?? (() => DateTimeOffset.UtcNow); public IReadOnlyList Items => _items; public event Action? Changed; public bool Add(string id, string kind, object payload) { if (_items.Any(item => item.Id == id && item.State != OverlayItemState.Removed)) return false; var value = settings(); if (_items.Count(item => item.State != OverlayItemState.Removed) >= Math.Max(value.QueueLimit + 1, value.QueueLimit * 2)) return false; var active = _items.Where(item => item.State is not OverlayItemState.Exiting and not OverlayItemState.Removed).ToList(); while (active.Count >= value.QueueLimit) { var oldest = active[0]; oldest.State = OverlayItemState.Exiting; oldest.StateAt = _now(); active.RemoveAt(0); } var created = new OverlayQueueItem(id, kind, payload, _now()) { State = OverlayItemState.Entering, StateAt = _now() }; _items.Add(created); Changed?.Invoke(); return true; } public void Advance() { var current = _now(); var value = settings(); var changed = false; foreach (var item in _items) { var elapsed = current - item.StateAt; if (item.State == OverlayItemState.Entering && elapsed.TotalMilliseconds >= value.EntryDurationMs) { item.State = OverlayItemState.Visible; item.StateAt = current; changed = true; } else if (item.State == OverlayItemState.Visible && elapsed.TotalMilliseconds >= value.LifetimeMs) { item.State = OverlayItemState.Exiting; item.StateAt = current; changed = true; } else if (item.State == OverlayItemState.Exiting && elapsed.TotalMilliseconds >= value.ExitDurationMs) { item.State = OverlayItemState.Removed; changed = true; } } if (_items.RemoveAll(item => item.State == OverlayItemState.Removed) > 0) changed = true; if (changed) Changed?.Invoke(); } public void Clear() { var current = _now(); foreach (var item in _items.Where(item => item.State is not OverlayItemState.Exiting and not OverlayItemState.Removed)) { item.State = OverlayItemState.Exiting; item.StateAt = current; } Changed?.Invoke(); } public void ExitWhere(Func predicate) { var current = _now(); var changed = false; foreach (var item in _items.Where(item => item.State is not OverlayItemState.Exiting and not OverlayItemState.Removed && predicate(item))) { item.State = OverlayItemState.Exiting; item.StateAt = current; changed = true; } if (changed) Changed?.Invoke(); } }