// Copyright (c) Arlo Godfrey. All Rights Reserved. // Licensed under the GNU Lesser General Public License, Version 3.0 with additional terms. // See the LICENSE, LICENSE.LESSER and LICENSE.ADDITIONAL files in the project root for more information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using Newtonsoft.Json; using OwlCore; using OwlCore.AbstractStorage; using OwlCore.Extensions; using StrixMusic.Sdk.FileMetadata.Models; namespace StrixMusic.Sdk.FileMetadata.Repositories { /// /// The service that helps in interacting with image information. /// public class ImageRepository : IImageRepository { private const string IMAGE_DATA_FILENAME = "ImageData.bin"; private readonly ConcurrentDictionary _inMemoryMetadata; private readonly SemaphoreSlim _storageMutex; private readonly SemaphoreSlim _initMutex; private readonly string _debouncerId; private IFolderData? _folderData; /// /// Creates a new instance of . /// public ImageRepository() { _inMemoryMetadata = new ConcurrentDictionary(); _storageMutex = new SemaphoreSlim(1, 1); _initMutex = new SemaphoreSlim(1, 1); _debouncerId = Guid.NewGuid().ToString(); } /// public bool IsInitialized { get; private set; } /// public event EventHandler>? MetadataUpdated; /// public event EventHandler>? MetadataRemoved; /// public event EventHandler>? MetadataAdded; /// public async Task InitAsync(CancellationToken cancellationToken = default) { using var initMutexReleaseRegistration = cancellationToken.Register(() => _initMutex.Release()); await _initMutex.WaitAsync(cancellationToken); if (IsInitialized) { _initMutex.Release(); return; } await LoadDataFromDiskAsync(cancellationToken); IsInitialized = true; _initMutex.Release(); } /// public Task GetItemCount() { return Task.FromResult(_inMemoryMetadata.Count); } /// public void SetDataFolder(IFolderData rootFolder) { _folderData = rootFolder; } /// public async Task AddOrUpdateAsync(params ImageMetadata[] metadata) { var addedImages = new List(); var updatedImages = new List(); await _storageMutex.WaitAsync(); var isUpdate = false; foreach (var item in metadata) { Guard.IsNotNullOrWhiteSpace(item.Id, nameof(item.Id)); _inMemoryMetadata.AddOrUpdate( item.Id, addValueFactory: id => { isUpdate = false; return item; }, updateValueFactory: (id, existing) => { isUpdate = true; return item; }); if (isUpdate) updatedImages.Add(item); else addedImages.Add(item); } _storageMutex.Release(); if (addedImages.Count > 0 || updatedImages.Count > 0) { _ = CommitChangesAsync(); if (addedImages.Count > 0) MetadataAdded?.Invoke(this, addedImages); if (updatedImages.Count > 0) MetadataUpdated?.Invoke(this, updatedImages); } } /// public async Task RemoveAsync(ImageMetadata imageMetadata) { Guard.IsNotNull(imageMetadata, nameof(imageMetadata)); Guard.IsNotNullOrWhiteSpace(imageMetadata.Id, nameof(imageMetadata.Id)); await _storageMutex.WaitAsync(); var removed = _inMemoryMetadata.TryRemove(imageMetadata.Id, out _); _storageMutex.Release(); if (removed) { _ = CommitChangesAsync(); MetadataRemoved?.Invoke(this, imageMetadata.IntoList()); } } /// public async Task GetByIdAsync(string id) { await _storageMutex.WaitAsync(); var result = _inMemoryMetadata[id]; _storageMutex.Release(); return result; } /// public Task> GetItemsAsync(int offset, int limit) { var allImages = _inMemoryMetadata.Values.ToList(); if (limit == -1) return Task.FromResult>(allImages); // If the offset exceeds the number of items we have, return nothing. if (offset >= allImages.Count) return Task.FromResult>(new List()); // If the total number of requested items exceeds the number of items we have, adjust the limit so it won't go out of range. if (offset + limit > allImages.Count) limit = allImages.Count - offset; return Task.FromResult>(allImages.GetRange(offset, limit)); } private async Task LoadDataFromDiskAsync(CancellationToken cancellationToken) { Guard.IsEmpty((ICollection>)_inMemoryMetadata, nameof(_inMemoryMetadata)); Guard.IsNotNull(_folderData, nameof(_folderData)); var fileData = await _folderData.CreateFileAsync(IMAGE_DATA_FILENAME, CreationCollisionOption.OpenIfExists); Guard.IsNotNull(fileData, nameof(fileData)); using var stream = await fileData.GetStreamAsync(FileAccessMode.ReadWrite); var bytes = await stream.ToBytesAsync(); if (bytes.Length == 0) return; var str = System.Text.Encoding.UTF8.GetString(bytes); cancellationToken.ThrowIfCancellationRequested(); var data = JsonConvert.DeserializeObject>(str); cancellationToken.ThrowIfCancellationRequested(); using var mutexReleaseOnCancelRegistration = cancellationToken.Register(() => _storageMutex.Release()); await _storageMutex.WaitAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); foreach (var item in data ?? Enumerable.Empty()) { Guard.IsNotNullOrWhiteSpace(item?.Id, nameof(item.Id)); if (!_inMemoryMetadata.TryAdd(item.Id, item)) ThrowHelper.ThrowInvalidOperationException($"Item already added to {nameof(_inMemoryMetadata)}"); } _storageMutex.Release(); } private async Task CommitChangesAsync() { if (!await Flow.Debounce(_debouncerId, TimeSpan.FromSeconds(5)) || _inMemoryMetadata.IsEmpty) return; await _storageMutex.WaitAsync(); Guard.IsNotNull(_folderData, nameof(_folderData)); var json = JsonConvert.SerializeObject(_inMemoryMetadata.Values.DistinctBy(x => x.Id).ToList()); var fileData = await _folderData.CreateFileAsync(IMAGE_DATA_FILENAME, CreationCollisionOption.OpenIfExists); await fileData.WriteAllBytesAsync(System.Text.Encoding.UTF8.GetBytes(json)); _storageMutex.Release(); } /// public void Dispose() { _initMutex.Dispose(); _storageMutex.Dispose(); } } }