// 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.AbstractUI.Models;
using OwlCore.Events;
using OwlCore.Extensions;
using OwlCore.Remoting;
using StrixMusic.Sdk.AppModels;
using StrixMusic.Sdk.CoreModels;
using StrixMusic.Sdk.MediaPlayback;
using StrixMusic.Sdk.Services;
namespace StrixMusic.Sdk.Plugins.CoreRemote
{
///
/// Wraps around an instance of an to enable controlling it remotely, or takes a remotingId to control another instance remotely.
///
///
/// Passing a core instance will enable remoting for the ENTIRE core, including library, search, playback, devices and all other feature.
///
[RemoteOptions(RemotingDirection.Bidirectional)]
public sealed class RemoteCore : ICore
{
private static readonly ConcurrentDictionary _hostCoreInstances
= new ConcurrentDictionary();
private static readonly ConcurrentDictionary _clientCoreInstances
= new ConcurrentDictionary();
private readonly MemberRemote _memberRemote;
private readonly ICore? _core;
private readonly object _devicesChangedLockObj = new object();
private readonly List _devices = new List();
private CoreState _coreState = CoreState.Unloaded;
private string _instanceDescriptor = string.Empty;
///
/// Creates a new instance of .
///
[JsonConstructor]
public RemoteCore(string instanceId)
{
if (!_clientCoreInstances.TryAdd(instanceId, this))
ThrowHelper.ThrowInvalidOperationException($"An instance with that ID already exists. Use RemoteCore.GetInstance(id) instead.");
InstanceId = instanceId;
// Dummy values to satisfy nullable. Will be overwritten remotely from other ctor.
RecentlyPlayed = null!;
Discoverables = null!;
Pins = null!;
Library = null!;
AbstractConfigPanel = new AbstractUICollection("TemporaryCollection");
// Registration is set remotely, use placeholder data here.
Registration = new CoreMetadata(string.Empty, string.Empty, new Uri("/", UriKind.Relative), sdkVer: new Version(0, 0, 0));
_memberRemote = new MemberRemote(this, $"{instanceId}.{nameof(RemoteCore)}", RemoteCoreMessageHandler.SingletonClient);
}
///
/// Wraps around and remotely relays events, property changes and method calls (with return data) from a core instance.
///
///
public RemoteCore(ICore core)
{
Guard.IsNotNull(core, nameof(core));
if (!_hostCoreInstances.TryAdd(core.InstanceId, this))
ThrowHelper.ThrowInvalidOperationException($"An instance with that ID already exists. Use RemoteCore.GetInstance(id) instead.");
_core = core;
_memberRemote = new MemberRemote(this, $"{core.InstanceId}.{nameof(RemoteCore)}", RemoteCoreMessageHandler.SingletonHost);
Library = new RemoteCoreLibrary(core.Library);
if (core.RecentlyPlayed is not null)
RecentlyPlayed = new RemoteCoreRecentlyPlayed(core.RecentlyPlayed);
if (core.Pins is not null)
Pins = new RemoteCorePlayableCollectionGroup(core.Pins);
if (core.Discoverables is not null)
Discoverables = new RemoteCoreDiscoverables(core.Discoverables);
ChangeDevices(core.Devices.Select((x, i) => new CollectionChangedItem(x, i)).ToList(), new List>());
AttachEvents(core);
AbstractConfigPanel = _core.AbstractConfigPanel;
Registration = core.Registration;
InstanceDescriptor = core.InstanceDescriptor;
InstanceId = core.InstanceId;
User = core.User;
}
///
/// Gets a created instance by instance ID.
///
/// The core instance.
///
public static RemoteCore GetInstance(string instanceId, RemotingMode mode)
{
if (mode is RemotingMode.Client)
{
if (!_clientCoreInstances.TryGetValue(instanceId, out var value))
return ThrowHelper.ThrowInvalidOperationException($"Could not find a registered {nameof(RemoteCore)} with {nameof(instanceId)} of {instanceId}");
return value;
}
if (mode is RemotingMode.Host)
{
if (!_hostCoreInstances.TryGetValue(instanceId, out var value))
return ThrowHelper.ThrowInvalidOperationException($"Could not find a registered {nameof(RemoteCore)} with {nameof(instanceId)} of {instanceId}");
return value;
}
return ThrowHelper.ThrowArgumentOutOfRangeException("Invalid remoting mode specified.");
}
private void AttachEvents(ICore core)
{
core.InstanceDescriptorChanged += OnInstanceDescriptorChanged;
core.DevicesChanged += OnDevicesChanged;
core.CoreStateChanged += OnCoreStateChanged;
}
private void DetachEvents(ICore core)
{
core.InstanceDescriptorChanged -= OnInstanceDescriptorChanged;
core.DevicesChanged -= OnDevicesChanged;
core.CoreStateChanged -= OnCoreStateChanged;
}
private void OnCoreStateChanged(object sender, CoreState e) => CoreState = e;
private void OnInstanceDescriptorChanged(object sender, string e)
{
InstanceDescriptor = e;
}
private void OnDevicesChanged(object sender, IReadOnlyList> addedItems, IReadOnlyList> removedItems)
{
ChangeDevices(addedItems, removedItems);
}
[RemoteMethod, RemoteOptions(RemotingDirection.HostToClient)]
private void ChangeDevices(IReadOnlyList> addedItems, IReadOnlyList> removedItems)
{
lock (_devicesChangedLockObj)
{
if (addedItems.Count + removedItems.Count == 0)
return;
var remoteAddedItems = addedItems.Select(x => new CollectionChangedItem(new RemoteCoreDevice(x.Data), x.Index)).ToList();
var remoteRemovedItems = removedItems.Select(x => new CollectionChangedItem(new RemoteCoreDevice(x.Data), x.Index)).ToList();
_devices.ChangeCollection(remoteAddedItems, remoteRemovedItems);
DevicesChanged?.Invoke(this, remoteAddedItems, remoteRemovedItems);
}
}
///
[RemoteProperty]
public CoreMetadata Registration { get; set; }
///
public string InstanceId { get; }
///
public ICore SourceCore => this;
///
[RemoteProperty]
public CoreState CoreState
{
get => _coreState;
set
{
_coreState = value;
CoreStateChanged?.Invoke(this, value);
}
}
///
[RemoteProperty]
public string InstanceDescriptor
{
get => _instanceDescriptor;
set
{
_instanceDescriptor = value;
InstanceDescriptorChanged?.Invoke(this, value);
}
}
///
[RemoteProperty]
public AbstractUICollection AbstractConfigPanel { get; private set; }
///
[RemoteProperty]
public MediaPlayerType PlaybackType { get; private set; }
///
[RemoteProperty]
public ICoreUser? User { get; set; }
///
[RemoteProperty]
public IReadOnlyList Devices => _devices;
///
[RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)]
public ICoreLibrary Library { get; set; }
///
[RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)]
public ICoreSearch? Search { get; set; }
///
[RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)]
public ICoreRecentlyPlayed? RecentlyPlayed { get; set; }
///
[RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)]
public ICoreDiscoverables? Discoverables { get; set; }
///
[RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)]
public ICorePlayableCollectionGroup? Pins { get; set; }
///
public event EventHandler? CoreStateChanged;
///
public event CollectionChangedEventHandler? DevicesChanged;
///
public event EventHandler? AbstractConfigPanelChanged;
///
public event EventHandler? InstanceDescriptorChanged;
///
[RemoteMethod]
public ValueTask DisposeAsync()
{
// Dispose any resources not known to the SDK.
// Do not dispose Library, Devices, etc. manually. The SDK will dispose these for you.
_clientCoreInstances.TryRemove(InstanceId, out _);
return default;
}
///
public Task InitAsync(CancellationToken cancellationToken = default) => Task.Run(async () =>
{
if (_memberRemote.Mode == RemotingMode.Host)
return;
await RemoteInitAsync();
IsInitialized = true;
});
///
public bool IsInitialized { get; private set; }
[RemoteMethod, RemoteOptions(RemotingDirection.ClientToHost)]
private Task RemoteInitAsync() => Task.Run(async () =>
{
if (_memberRemote.Mode == RemotingMode.Client)
{
await _memberRemote.RemoteWaitAsync(nameof(InitAsync));
return;
}
Guard.IsNotNull(_core, nameof(_core));
await _core.InitAsync();
await _memberRemote.RemoteReleaseAsync(nameof(InitAsync));
});
///
[RemoteMethod, RemoteOptions(RemotingDirection.ClientToHost)]
public Task GetContextByIdAsync(string id, CancellationToken cancellationToken = default) => Task.Run(async () =>
{
var methodCallToken = $"{nameof(GetContextByIdAsync)}.{id}";
if (_memberRemote.Mode == RemotingMode.Host)
{
Guard.IsNotNull(_core, nameof(_core));
var result = await _core.GetContextByIdAsync(id, cancellationToken);
ICoreMember? remoteEnabledResult = result switch
{
ICore core => GetInstance(core.InstanceId, _memberRemote.Mode),
ICoreAlbum album => new RemoteCoreAlbum(album),
ICoreArtist artist => new RemoteCoreArtist(artist),
ICoreTrack track => new RemoteCoreTrack(track),
ICorePlaylist playlist => new RemoteCorePlaylist(playlist),
ICoreDevice device => new RemoteCoreDevice(device),
ICoreDiscoverables discoverables => new RemoteCoreDiscoverables(discoverables),
ICoreImage image => new RemoteCoreImage(image),
ICoreLibrary library => new RemoteCoreLibrary(library),
ICoreRecentlyPlayed recentlyPlayed => new RemoteCoreRecentlyPlayed(recentlyPlayed),
ICoreSearchHistory searchHistory => new RemoteCoreSearchHistory(searchHistory),
ICorePlayableCollectionGroup collectionGroup => new RemoteCorePlayableCollectionGroup(collectionGroup),
_ => throw new NotImplementedException(),
};
return await _memberRemote.PublishDataAsync(methodCallToken, remoteEnabledResult);
}
else if (_memberRemote.Mode == RemotingMode.Client)
{
return await _memberRemote.ReceiveDataAsync(methodCallToken);
}
else
{
return null;
}
});
///
[RemoteMethod, RemoteOptions(RemotingDirection.ClientToHost)]
public Task GetMediaSourceAsync(ICoreTrack track, CancellationToken cancellationToken = default) => Task.Run(async () =>
{
var methodCallToken = $"{nameof(GetMediaSourceAsync)}.{track.Id}";
if (_memberRemote.Mode == RemotingMode.Host)
{
Guard.IsNotNull(_core, nameof(_core));
var result = await _core.GetMediaSourceAsync(track);
return await _memberRemote.PublishDataAsync(methodCallToken, result);
}
else if (_memberRemote.Mode == RemotingMode.Client)
{
return await _memberRemote.ReceiveDataAsync(methodCallToken);
}
else
{
return null;
}
});
}
}