// 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.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using OwlCore; using OwlCore.Events; using OwlCore.Extensions; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.CoreModels; namespace StrixMusic.Sdk.AdapterModels { /// /// Aggregates many instances into one instance. /// All data returned and emitted by this instance will contained merged data from the provided sources. /// public sealed class MergedCore : IStrixDataRoot, IMergedMutable { private readonly List _sources; private readonly List _devices; private readonly MergedLibrary _library; private MergedDiscoverables? _discoverables; private MergedPlayableCollectionGroup? _pins; private MergedRecentlyPlayed? _recentlyPlayed; private MergedSearch? _search; /// /// Initializes a new instance of . /// public MergedCore(IEnumerable cores, MergedCollectionConfig config) { _sources = cores.ToList(); Guard.HasSizeGreaterThan(_sources, 0, nameof(_sources)); foreach (var core in _sources) CheckSdkVersion(core.Registration); MergeConfig = config; _library = new MergedLibrary(_sources.Select(x => x.Library), config); // These items have no notification support for changing after construction, // so they need to be initialized every time in case a new source is added with a non-null value. if (_sources.Any(x => x.Discoverables != null)) _discoverables = new MergedDiscoverables(_sources.Select(x => x.Discoverables).PruneNull(), config); if (_sources.Any(x => x.Pins != null)) _pins = new MergedPlayableCollectionGroup(_sources.Select(x => x.Pins).PruneNull(), config); if (_sources.Any(x => x.RecentlyPlayed != null)) _recentlyPlayed = new MergedRecentlyPlayed(_sources.Select(x => x.RecentlyPlayed).PruneNull(), config); if (_sources.Any(x => x.Search != null)) _search = new MergedSearch(_sources.Select(x => x.Search).PruneNull(), config); _devices = new List(_sources.SelectMany(x => x.Devices, (_, device) => new DeviceAdapter(device))); foreach (var source in _sources) AttachEvents(source); } private void AttachEvents(ICore core) { core.DevicesChanged += Core_DevicesChanged; } private void DetachEvents(ICore core) { core.DevicesChanged -= Core_DevicesChanged; } /// public event CollectionChangedEventHandler? DevicesChanged; /// public event EventHandler? SourcesChanged; /// public event EventHandler? PinsChanged; /// public event EventHandler? SearchChanged; /// public event EventHandler? RecentlyPlayedChanged; /// public event EventHandler? DiscoverablesChanged; private void Core_DevicesChanged(object sender, IReadOnlyList> addedItems, IReadOnlyList> removedItems) { var itemsToAdd = addedItems.Select(x => new CollectionChangedItem(new DeviceAdapter(x.Data), x.Index)).ToList(); var itemsToRemove = removedItems.Select(x => new CollectionChangedItem(new DeviceAdapter(x.Data), x.Index)).ToList(); #warning TODO: Compute actual indices for merged device list. foreach (var item in itemsToRemove) _devices.RemoveAt(item.Index); foreach (var item in itemsToAdd) _devices.InsertOrAdd(item.Index, item.Data); DevicesChanged?.Invoke(this, itemsToAdd, itemsToRemove); } /// public IReadOnlyList Sources => _sources; /// public MergedCollectionConfig MergeConfig { get; } /// public IReadOnlyList Devices => _devices; /// public ILibrary Library => _library; /// public IPlayableCollectionGroup? Pins => _pins; /// public ISearch? Search => _search; /// public IRecentlyPlayed? RecentlyPlayed => _recentlyPlayed; /// public IDiscoverables? Discoverables => _discoverables; /// public bool IsInitialized { get; private set; } /// public async Task InitAsync(CancellationToken cancellationToken = default) { foreach (var core in _sources) await InitCore(core, cancellationToken); IsInitialized = true; } private async Task InitCore(ICore core, CancellationToken cancellationToken) { Begin: var setupCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); if (core.CoreState == CoreState.Unloaded || core.CoreState == CoreState.NeedsConfiguration) { try { await core.InitAsync(setupCancellationTokenSource.Token); } catch (OperationCanceledException) { if (!setupCancellationTokenSource.IsCancellationRequested) setupCancellationTokenSource.Cancel(); } } if (setupCancellationTokenSource.IsCancellationRequested || core.CoreState == CoreState.Unloaded) { setupCancellationTokenSource.Dispose(); RemoveSource(core); return; } if (core.CoreState == CoreState.NeedsConfiguration) { // If the user needs to provide information for setup to continue, wait for another state change. // Display of the core's AbstractConfigPanel handled by the host application. await Flow.EventAsTask(cb => core.CoreStateChanged += cb, cb => core.CoreStateChanged -= cb, TimeSpan.FromMinutes(10)); goto Begin; } setupCancellationTokenSource.Dispose(); } /// /// /// Cores can be merged, but are never matched conditionally. /// public bool Equals(ICore other) => false; /// public void AddSource(ICore itemToMerge) { if (_sources.Contains(itemToMerge)) ThrowHelper.ThrowArgumentException(nameof(itemToMerge), "Cannot add the same source twice."); CheckSdkVersion(itemToMerge.Registration); _devices.AddRange(itemToMerge.Devices.Select(x => new DeviceAdapter(x))); _library.AddSource(itemToMerge.Library); if (itemToMerge.Discoverables is not null) { if (_discoverables is not null) { _discoverables.AddSource(itemToMerge.Discoverables); } else { _discoverables = new MergedDiscoverables(itemToMerge.Discoverables.IntoList(), MergeConfig); DiscoverablesChanged?.Invoke(this, _discoverables); } } if (itemToMerge.RecentlyPlayed is not null) { if (_recentlyPlayed is not null) { _recentlyPlayed.AddSource(itemToMerge.RecentlyPlayed); } else { _recentlyPlayed = new MergedRecentlyPlayed(itemToMerge.RecentlyPlayed.IntoList(), MergeConfig); RecentlyPlayedChanged?.Invoke(this, _recentlyPlayed); } } if (itemToMerge.Pins is not null) { if (_pins is not null) { _pins.AddSource(itemToMerge.Pins); } else { _pins = new MergedPlayableCollectionGroup(itemToMerge.Pins.IntoList(), MergeConfig); PinsChanged?.Invoke(this, _pins); } } if (itemToMerge.Search is not null) { if (_search is not null) { _search.AddSource(itemToMerge.Search); } else { _search = new MergedSearch(itemToMerge.Search.IntoList(), MergeConfig); SearchChanged?.Invoke(this, _search); } } _sources.Add(itemToMerge); AttachEvents(itemToMerge); SourcesChanged?.Invoke(this, EventArgs.Empty); } /// public void RemoveSource(ICore itemToRemove) { if (!_sources.Contains(itemToRemove)) ThrowHelper.ThrowArgumentException(nameof(itemToRemove), "Cannot remove an item that doesn't exist in the collection."); _devices.RemoveAll(x => itemToRemove.Devices.Any(y => y.Id == x.Id) && x.SourceCore?.InstanceId == itemToRemove.InstanceId); _library.RemoveSource(itemToRemove.Library); if (itemToRemove.Discoverables is not null) { Guard.IsNotNull(_discoverables); _discoverables.RemoveSource(itemToRemove.Discoverables); } if (itemToRemove.RecentlyPlayed is not null) { Guard.IsNotNull(_recentlyPlayed); _recentlyPlayed.RemoveSource(itemToRemove.RecentlyPlayed); } if (itemToRemove.Pins is not null) { Guard.IsNotNull(_pins); _pins.RemoveSource(itemToRemove.Pins); } if (itemToRemove.Search is not null) { Guard.IsNotNull(_search); _search.RemoveSource(itemToRemove.Search); } _sources.Remove(itemToRemove); DetachEvents(itemToRemove); SourcesChanged?.Invoke(this, EventArgs.Empty); } /// public async ValueTask DisposeAsync() { if (IsInitialized) return; foreach (var source in _sources) { DetachEvents(source); await source.DisposeAsync(); } IsInitialized = false; } private void CheckSdkVersion(CoreMetadata coreMetadata) { var currentSdkVersion = typeof(ICore).Assembly.GetName().Version; if (coreMetadata.SdkVer != currentSdkVersion) throw new IncompatibleSdkVersionException(coreMetadata.SdkVer, coreMetadata.DisplayName); } } }