using System.Text.RegularExpressions; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Infrastructure.Search; public sealed class SmartCollectionCache(IDbContextFactory dbContextFactory) : ISmartCollectionCache, IDisposable { private readonly Dictionary _data = new(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); private readonly AdjGraph _graph = new(); public async Task Refresh() { await _semaphoreSlim.WaitAsync(); try { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); List smartCollections = await dbContext.SmartCollections .AsNoTracking() .ToListAsync(); _data.Clear(); _graph.Clear(); foreach (SmartCollection smartCollection in smartCollections) { var data = new SmartCollectionData(smartCollection.Query); _data.Add(smartCollection.Name, data); foreach (Match match in SearchQueryParser.SmartCollectionRegex().Matches(smartCollection.Query)) { string otherCollectionName = match.Groups[1].Value; _graph.AddEdge(smartCollection.Name, otherCollectionName); } } foreach (SmartCollection smartCollection in smartCollections) { SmartCollectionData data = _data[smartCollection.Name]; data.HasCycle = _graph.HasCycle(smartCollection.Name); } } finally { _semaphoreSlim.Release(); } } public async Task HasCycle(string name) { await _semaphoreSlim.WaitAsync(); try { return _data.TryGetValue(name, out SmartCollectionData data) && data.HasCycle; } finally { _semaphoreSlim.Release(); } } public async Task> GetQuery(string name) { await _semaphoreSlim.WaitAsync(); try { return _data.TryGetValue(name, out SmartCollectionData data) ? data.Query : Option.None; } finally { _semaphoreSlim.Release(); } } public void Dispose() { _semaphoreSlim.Dispose(); } private record SmartCollectionData(string Query) { public bool HasCycle { get; set; } } }