using System.Reactive.Linq; using System.Reactive.Subjects; using System.Text.Json; using DeclarativeProperty; using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.FrequencyNavigation.Models; using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.Core.Models; using FileTime.Core.Models.Extensions; using FileTime.Core.Services; using Microsoft.Extensions.Logging; using PropertyChanged.SourceGenerator; namespace FileTime.App.FrequencyNavigation.Services; public partial class FrequencyNavigationService : IFrequencyNavigationService, IStartupHandler, IExitHandler { private const int MaxAge = 10_000; private DateTime _lastSave = DateTime.Now; private readonly ILogger _logger; private readonly IModalService _modalService; private readonly SemaphoreSlim _saveLock = new(1, 1); private Dictionary _containerScores = new(); private readonly DeclarativeProperty _showWindow = new(false); private readonly string _dbPath; [Notify] IFrequencyNavigationViewModel? _currentModal; IDeclarativeProperty IFrequencyNavigationService.ShowWindow => _showWindow; public FrequencyNavigationService( ITabEvents tabEvents, IApplicationSettings applicationSettings, ILogger logger, IModalService modalService) { _logger = logger; _modalService = modalService; _dbPath = Path.Combine(applicationSettings.AppDataRoot, "frequencyNavigationScores.json"); tabEvents.LocationChanged += OnTabLocationChanged; } async void OnTabLocationChanged(object? sender, TabLocationChanged e) { try { await IncreaseContainerScore(e.Location); } catch (Exception ex) { _logger.LogError(ex, "Fatal error while increasing container score"); } } public async Task OpenNavigationWindow() { await _showWindow.SetValue(true); CurrentModal = _modalService.OpenModal(); } public void CloseNavigationWindow() { _showWindow.SetValueSafe(false); if (_currentModal is not null) { _modalService.CloseModal(_currentModal); CurrentModal = null; } } private async Task IncreaseContainerScore(IContainer container) { await _saveLock.WaitAsync(); try { if (container.GetExtension() is not null) return; var containerNameString = container.FullName?.Path; if (containerNameString is null) return; if (_containerScores.ContainsKey(containerNameString)) { _containerScores[containerNameString].Score++; _containerScores[containerNameString].LastAccessed = DateTime.Now; } else { _containerScores.Add(containerNameString, new ContainerFrequencyData()); } } catch (Exception e) { _logger.LogError(e, "Error increasing container score"); } finally { _saveLock.Release(); } try { if (TryAgeContainerScores() || DateTime.Now - _lastSave > TimeSpan.FromMinutes(5)) { } //TODO: move to if above await SaveStateAsync(); } catch (Exception e) { _logger.LogError(e, "Error aging container scores"); } } private bool TryAgeContainerScores() { if (_containerScores.Select(c => c.Value.Score).Sum() < MaxAge) return false; AgeContainerScores(); return true; } private void AgeContainerScores() { var now = DateTime.Now; var itemsToRemove = new List(); foreach (var container in _containerScores) { var newScore = (int) Math.Floor(container.Value.Score * 0.9); if (newScore > 0) { container.Value.Score = newScore; } else { itemsToRemove.Add(container.Key); } } foreach (var itemToRemove in itemsToRemove) { _containerScores.Remove(itemToRemove); } } public IList GetMatchingContainers(string searchText) { if (string.IsNullOrWhiteSpace(searchText)) return new List(); _saveLock.Wait(); try { return _containerScores .Where(c => c.Key.Contains(searchText, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed)) .Select(c => c.Key) .ToList(); } finally { _saveLock.Release(); } } private int GetWeightedScore(int score, DateTime lastAccess) { var now = DateTime.Now; var timeSinceLastAccess = now - lastAccess; return timeSinceLastAccess.TotalHours switch { < 1 => score *= 4, < 24 => score *= 2, < 168 => score /= 2, _ => score /= 4 }; } public async Task InitAsync() => await LoadStateAsync(); private async Task LoadStateAsync() { if (!File.Exists(_dbPath)) return; try { await _saveLock.WaitAsync(); _logger.LogTrace("Loading frequency navigation state from file '{DbPath}'", _dbPath); await using var dbStream = File.OpenRead(_dbPath); var containerScores = await JsonSerializer.DeserializeAsync>(dbStream); if (containerScores is null) return; _containerScores = containerScores; } catch (Exception e) { _logger.LogError(e, "Error loading frequency navigation state"); } finally { _saveLock.Release(); } } public async Task ExitAsync(CancellationToken token = default) => await SaveStateAsync(token); private async Task SaveStateAsync(CancellationToken token = default) { await _saveLock.WaitAsync(token); try { _lastSave = DateTime.Now; await using var dbStream = File.Create(_dbPath); await JsonSerializer.SerializeAsync(dbStream, _containerScores); dbStream.Flush(); } finally { _saveLock.Release(); } } }