using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
namespace Artemis.Core;
///
/// Represents a profile containing folders and layers
///
public sealed class Profile : ProfileElement
{
private readonly object _lock = new();
private readonly ObservableCollection _scriptConfigurations;
private readonly ObservableCollection _scripts;
private bool _isFreshImport;
private ProfileElement? _lastSelectedProfileElement;
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
_scripts = new ObservableCollection();
_scriptConfigurations = new ObservableCollection();
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
Exceptions = new List();
Scripts = new ReadOnlyObservableCollection(_scripts);
ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations);
Load();
}
///
/// Gets the profile configuration of this profile
///
public ProfileConfiguration Configuration { get; }
///
/// Gets a collection of all active scripts assigned to this profile
///
public ReadOnlyObservableCollection Scripts { get; }
///
/// Gets a collection of all script configurations assigned to this profile
///
public ReadOnlyObservableCollection ScriptConfigurations { get; }
///
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
///
/// Note: As long as this is , profile adaption will be performed on load and any surface
/// changes
///
///
public bool IsFreshImport
{
get => _isFreshImport;
set => SetAndNotify(ref _isFreshImport, value);
}
///
/// Gets or sets the last selected profile element of this profile
///
public ProfileElement? LastSelectedProfileElement
{
get => _lastSelectedProfileElement;
set => SetAndNotify(ref _lastSelectedProfileElement, value);
}
///
/// Gets the profile entity this profile uses for persistent storage
///
public ProfileEntity ProfileEntity { get; internal set; }
internal List Exceptions { get; }
///
public override void Update(double deltaTime)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdating(deltaTime);
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
}
}
///
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition, editorFocus);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
if (!Exceptions.Any())
return;
List exceptions = new(Exceptions);
Exceptions.Clear();
throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions);
}
}
///
public override void Reset()
{
foreach (ProfileElement child in Children)
child.Reset();
}
///
/// Retrieves the root folder of this profile
///
/// The root folder of the profile
///
public Folder GetRootFolder()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
return (Folder) Children.Single();
}
///
public override string ToString()
{
return $"[Profile] {nameof(Name)}: {Name}";
}
///
/// Populates all the LEDs on the elements in this profile
///
/// The devices to use while populating LEDs
public void PopulateLeds(IEnumerable devices)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())
layer.PopulateLeds(devices);
}
#region Overrides of BreakableModel
///
public override IEnumerable GetBrokenHierarchy()
{
return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy());
}
#endregion
///
protected override void Dispose(bool disposing)
{
if (!disposing)
return;
while (Scripts.Count > 0)
RemoveScript(Scripts[0]);
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
Disposed = true;
}
internal override void Load()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList)
{
// Remove the old profile tree
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
// Populate the profile starting at the root, the rest is populated recursively
FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId);
if (rootFolder == null)
AddChild(new Folder(this, "Root folder"));
else
AddChild(new Folder(this, this, rootFolder));
}
List renderElements = GetAllRenderElements();
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
else
LastSelectedProfileElement = null;
while (_scriptConfigurations.Any())
RemoveScriptConfiguration(_scriptConfigurations[0]);
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
AddScriptConfiguration(scriptConfiguration);
// Load node scripts last since they may rely on the profile structure being in place
foreach (RenderProfileElement renderProfileElement in renderElements)
renderProfileElement.LoadNodeScript();
}
///
/// Removes a script configuration from the profile, if the configuration has an active script it is also removed.
///
internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
return;
Script? script = scriptConfiguration.Script;
if (script != null)
RemoveScript((ProfileScript) script);
_scriptConfigurations.Remove(scriptConfiguration);
}
///
/// Adds a script configuration to the profile but does not instantiate it's script.
///
internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
_scriptConfigurations.Add(scriptConfiguration);
}
///
/// Adds a script that has a script configuration belonging to this profile.
///
internal void AddScript(ProfileScript script)
{
if (!_scriptConfigurations.Contains(script.ScriptConfiguration))
throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile.");
if (!_scripts.Contains(script))
_scripts.Add(script);
}
///
/// Removes a script from the profile and disposes it.
///
internal void RemoveScript(ProfileScript script)
{
_scripts.Remove(script);
script.Dispose();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
ProfileEntity.Name = Configuration.Name;
ProfileEntity.IsFreshImport = IsFreshImport;
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
foreach (ProfileElement profileElement in Children)
profileElement.Save();
ProfileEntity.Folders.Clear();
ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity));
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
ProfileEntity.ScriptConfigurations.Clear();
foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
{
scriptConfiguration.Save();
ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
}
}
}