Code/RecentLinks.cs
namespace Bimp;

/// <summary>
/// Links this client has seen played on any media player, newest first, saved to <see cref="FileSystem.Data"/> so they
/// survive restarts. The remote's "Recent" list plays or queues one again - the way in for a controller, which can't
/// type or paste a link (games can't read the clipboard, only a focused text box can take Ctrl+V).
/// </summary>
public static class RecentLinks
{
	public sealed class Entry
	{
		/// <summary> The link as it was entered (page url, not the resolved stream). </summary>
		public string Url { get; set; }
		public string Title { get; set; }

		/// <summary> Seconds, 0 if unknown. </summary>
		public float Duration { get; set; }
		public bool IsLive { get; set; }
	}

	const int MaxEntries = 30;
	const string FilePath = "bimp/recent.json";

	static List<Entry> entries;

	/// <summary> The PlayId and duration last recorded for each player. </summary>
	static readonly Dictionary<Guid, (int playId, float duration)> seen = new();

	/// <summary> Changes whenever the list does (for UI rebuilds). </summary>
	public static int Version { get; private set; }

	/// <summary> Newest first. </summary>
	public static IReadOnlyList<Entry> All => Load();

	/// <summary>
	/// Call every frame for each player: records what it's playing when it starts something (or learns its duration).
	/// </summary>
	public static void Saw( MediaPlayer player )
	{
		if ( !player.HasMedia || string.IsNullOrWhiteSpace( player.CurrentUrl ) ) return;

		var now = (player.PlayId, player.MediaDuration);
		if ( seen.TryGetValue( player.Id, out var last ) && last == now ) return;
		seen[player.Id] = now;

		var list = Load();
		list.RemoveAll( e => string.Equals( e.Url, player.CurrentUrl, StringComparison.OrdinalIgnoreCase ) );
		list.Insert( 0, new Entry
		{
			Url = player.CurrentUrl,
			Title = string.IsNullOrWhiteSpace( player.Title ) ? player.CurrentUrl : player.Title,
			Duration = player.MediaDuration,
			IsLive = player.IsLive,
		} );
		if ( list.Count > MaxEntries ) list.RemoveRange( MaxEntries, list.Count - MaxEntries );

		Version++;
		Save();
	}

	/// <summary> Forget one link. </summary>
	public static void Remove( string url )
	{
		if ( Load().RemoveAll( e => e.Url == url ) == 0 ) return;
		Version++;
		Save();
	}

	static List<Entry> Load()
	{
		if ( entries is not null ) return entries;
		try
		{
			entries = FileSystem.Data.FileExists( FilePath ) ? FileSystem.Data.ReadJson<List<Entry>>( FilePath ) : null;
		}
		catch ( Exception e )
		{
			Log.Warning( $"[bimp] couldn't read the recent links: {e.Message}" );
		}
		entries ??= new();
		entries.RemoveAll( e => string.IsNullOrWhiteSpace( e?.Url ) );
		return entries;
	}

	static void Save()
	{
		try
		{
			FileSystem.Data.CreateDirectory( "bimp" );
			FileSystem.Data.WriteJson( FilePath, entries );
		}
		catch ( Exception e )
		{
			Log.Warning( $"[bimp] couldn't save the recent links: {e.Message}" );
		}
	}
}