// 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();
}
}
}