📄 src/App/ViewModels/Panels/VasttrafikPanelViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using MMirror.Integrations.Vasttrafik;

namespace MMirror.App.ViewModels.Panels;

public partial class VasttrafikPanelViewModel : ViewModelBase
{
    private readonly TimeProvider timeProvider;
    private readonly VasttrafikClient vasttrafikClient;
    private readonly DispatcherTimer secondTimer;

    private CancellationTokenSource cts = new();

    [ObservableProperty]
    public partial DateTimeOffset CurrentTime { get; private set; }

    [ObservableProperty]
    public partial ObservableCollection<Departure> Departures { get; private set; }

    [ObservableProperty]
    public required partial string StopName { get; set; }

    public required string StopId { get; set; }

    public VasttrafikPanelViewModel(TimeProvider timeProvider, VasttrafikClient vasttrafikClient)
    {
        this.timeProvider = timeProvider;
        this.vasttrafikClient = vasttrafikClient;

        CurrentTime = timeProvider.GetLocalNow();
        Departures = [];

        secondTimer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(15) };
        secondTimer.Tick += OnSecondTick;
    }

    public void SubscribeToUpdates()
    {
        secondTimer.Start();
        Task.Run(() => FetchLoop(cts.Token));
    }

    private void OnSecondTick(object? sender, EventArgs e)
    {
        CurrentTime = timeProvider.GetLocalNow();
        Departures =
        [
            .. Departures
                .Where(d => CurrentTime < d.Next || (d.NextNext.HasValue && CurrentTime < d.NextNext.Value))
                .Select(d => CurrentTime < d.Next ? d : d with { Next = d.NextNext!.Value, NextNext = null })
                .OrderBy(d => d.Next),
        ];
    }

    private async Task FetchLoop(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            Departures = [.. await vasttrafikClient.GetDeparturesFrom(StopId, cancellationToken)];
            CurrentTime = timeProvider.GetLocalNow();

            await Task.Delay(TimeSpan.FromMinutes(1), timeProvider, cancellationToken);
        }
    }

    public void UnsubscribeFromUpdates()
    {
        secondTimer.Stop();
        cts.Cancel();
        cts = new();
    }
}

internal static class VasttrafikPanelViewModelServiceCollectionExtensions
{
    extension(IServiceCollection services)
    {
        public IServiceCollection AddVasttrafikPanelViewModel()
        {
            services.AddTransient<VasttrafikPanelViewModel>();
            return services;
        }
    }
}