using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
using RGB.NET.Core;
using SkiaSharp;

namespace Artemis.Core;

/// <summary>
///     Represents a layer in a <see cref="Profile" />
/// </summary>
public sealed class Layer : RenderProfileElement
{
    private const string BROKEN_STATE_BRUSH_NOT_FOUND = "Failed to load layer brush, ensure the plugin is enabled";
    private const string BROKEN_STATE_INIT_FAILED = "Failed to initialize layer brush";
    
    private readonly List<Layer> _renderCopies = new();
    private LayerGeneralProperties _general = new();
    private LayerTransformProperties _transform = new();
    private BaseLayerBrush? _layerBrush;
    private LayerShape? _layerShape;
    private List<ArtemisLed> _leds = new();
    private List<LedEntity> _missingLeds = new();

    /// <summary>
    ///     Creates a new instance of the <see cref="Layer" /> class and adds itself to the child collection of the provided
    ///     <paramref name="parent" />
    /// </summary>
    /// <param name="parent">The parent of the layer</param>
    /// <param name="name">The name of the layer</param>
    public Layer(ProfileElement parent, string name) : base(parent, parent.Profile)
    {
        LayerEntity = new LayerEntity();
        EntityId = Guid.NewGuid();

        Profile = Parent.Profile;
        Name = name;
        Suspended = false;
        Leds = new ReadOnlyCollection<ArtemisLed>(_leds);
        Adapter = new LayerAdapter(this);

        Initialize();
    }

    /// <summary>
    ///     Creates a new instance of the <see cref="Layer" /> class based on the provided layer entity
    /// </summary>
    /// <param name="profile">The profile the layer belongs to</param>
    /// <param name="parent">The parent of the layer</param>
    /// <param name="layerEntity">The entity of the layer</param>
    /// <param name="loadNodeScript">A boolean indicating whether or not to attempt to load the node script straight away</param>
    public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity, bool loadNodeScript = false) : base(parent, parent.Profile)
    {
        LayerEntity = layerEntity;
        EntityId = layerEntity.Id;

        Profile = profile;
        Parent = parent;
        Leds = new ReadOnlyCollection<ArtemisLed>(_leds);
        Adapter = new LayerAdapter(this);

        Load();
        Initialize();
        if (loadNodeScript)
            LoadNodeScript();
    }

    /// <summary>
    ///     Creates a new instance of the <see cref="Layer" /> class by copying the provided <paramref name="source" />.
    /// </summary>
    /// <param name="source">The layer to copy</param>
    private Layer(Layer source) : base(source, source.Profile)
    {
        LayerEntity = source.LayerEntity;

        Profile = source.Profile;
        Parent = source;

        // TODO: move to top
        _renderCopies = new List<Layer>();
        _general = new LayerGeneralProperties();
        _transform = new LayerTransformProperties();

        _leds = new List<ArtemisLed>();
        Leds = new ReadOnlyCollection<ArtemisLed>(_leds);

        Adapter = new LayerAdapter(this);
        Load();
        Initialize();

        Timeline.JumpToStart();
        AddLeds(source.Leds);
        Enable();

        // After loading using the source entity create a new entity so the next call to Save won't mess with the source, just in case.
        LayerEntity = new LayerEntity();
    }

    /// <summary>
    ///     A collection of all the LEDs this layer is assigned to.
    /// </summary>
    public ReadOnlyCollection<ArtemisLed> Leds { get; private set; }

    /// <summary>
    ///     Defines the shape that is rendered by the <see cref="LayerBrush" />.
    /// </summary>
    public LayerShape? LayerShape
    {
        get => _layerShape;
        set
        {
            SetAndNotify(ref _layerShape, value);
            if (Path != null)
                CalculateRenderProperties();
        }
    }

    /// <summary>
    ///     Gets the general properties of the layer
    /// </summary>
    [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")]
    public LayerGeneralProperties General
    {
        get => _general;
        private set => SetAndNotify(ref _general, value);
    }

    /// <summary>
    ///     Gets the transform properties of the layer
    /// </summary>
    [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")]
    public LayerTransformProperties Transform
    {
        get => _transform;
        private set => SetAndNotify(ref _transform, value);
    }

    /// <summary>
    ///     The brush that will fill the <see cref="LayerShape" />.
    /// </summary>
    public BaseLayerBrush? LayerBrush
    {
        get => _layerBrush;
        internal set => SetAndNotify(ref _layerBrush, value);
    }

    /// <summary>
    ///     Gets the layer entity this layer uses for persistent storage
    /// </summary>
    public LayerEntity LayerEntity { get; internal set; }

    /// <summary>
    ///     Gets the layer adapter that can be used to adapt this layer to a different set of devices
    /// </summary>
    public LayerAdapter Adapter { get; }

    /// <inheritdoc />
    public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet;

    internal override RenderElementEntity RenderElementEntity => LayerEntity;

    /// <inheritdoc />
    public override List<ILayerProperty> GetAllLayerProperties()
    {
        List<ILayerProperty> result = new();
        result.AddRange(General.GetAllLayerProperties());
        result.AddRange(Transform.GetAllLayerProperties());
        if (LayerBrush?.BaseProperties != null)
            result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties());
        foreach (BaseLayerEffect layerEffect in LayerEffects)
        {
            if (layerEffect.BaseProperties != null)
                result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
        }

        return result;
    }

    /// <inheritdoc />
    public override string ToString()
    {
        return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
    }

    /// <summary>
    ///     Occurs when a property affecting the rendering properties of this layer has been updated
    /// </summary>
    public event EventHandler? RenderPropertiesUpdated;

    /// <summary>
    ///     Occurs when the layer brush of this layer has been updated
    /// </summary>
    public event EventHandler? LayerBrushUpdated;

    #region Overrides of BreakableModel

    /// <inheritdoc />
    public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
    {
        if (LayerBrush?.BrokenState != null)
            yield return LayerBrush;
        foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null))
            yield return baseLayerEffect;
    }

    #endregion

    /// <inheritdoc />
    protected override void Dispose(bool disposing)
    {
        Disable();

        Disposed = true;

        LayerBrushStore.LayerBrushAdded -= LayerBrushStoreOnLayerBrushAdded;
        LayerBrushStore.LayerBrushRemoved -= LayerBrushStoreOnLayerBrushRemoved;

        // Brush first in case it depends on any of the other disposables during it's own disposal
        _layerBrush?.Dispose();
        _general.Dispose();
        _transform.Dispose();

        base.Dispose(disposing);
    }

    internal void OnLayerBrushUpdated()
    {
        LayerBrushUpdated?.Invoke(this, EventArgs.Empty);
    }

    private void Initialize()
    {
        LayerBrushStore.LayerBrushAdded += LayerBrushStoreOnLayerBrushAdded;
        LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved;

        // Layers have two hardcoded property groups, instantiate them
        PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute(
            GetType().GetProperty(nameof(General))!,
            typeof(PropertyGroupDescriptionAttribute)
        )!;
        PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute(
            GetType().GetProperty(nameof(Transform))!,
            typeof(PropertyGroupDescriptionAttribute)
        )!;

        LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier};
        LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier};

        General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup);
        Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup);

        General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet;
        ApplyShapeType();
        ActivateLayerBrush();

        Reset();
    }

    private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e)
    {
        if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor)
        {
            DeactivateLayerBrush();
            SetBrokenState(BROKEN_STATE_BRUSH_NOT_FOUND);
        }
    }

    private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e)
    {
        if (LayerBrush != null || !General.PropertiesInitialized)
            return;

        LayerBrushReference? current = General.BrushReference.CurrentValue;
        if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType)
            ActivateLayerBrush();
    }

    private void OnRenderPropertiesUpdated()
    {
        RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty);
    }

    #region Storage

    internal override void Load()
    {
        EntityId = LayerEntity.Id;
        Name = LayerEntity.Name;
        Suspended = LayerEntity.Suspended;
        Order = LayerEntity.Order;

        LoadRenderElement();
        Adapter.Load();
    }

    internal override void Save()
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        // Properties
        LayerEntity.Id = EntityId;
        LayerEntity.ParentId = Parent?.EntityId ?? new Guid();
        LayerEntity.Order = Order;
        LayerEntity.Suspended = Suspended;
        LayerEntity.Name = Name;
        LayerEntity.ProfileId = Profile.EntityId;

        General.ApplyToEntity();
        Transform.ApplyToEntity();

        // Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush
        if (LayerBrush != null)
        {
            LayerBrush.Save();
            LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity;
        }

        // LEDs
        LayerEntity.Leds.Clear();
        SaveMissingLeds();
        foreach (ArtemisLed artemisLed in Leds)
        {
            LedEntity ledEntity = new()
            {
                DeviceIdentifier = artemisLed.Device.Identifier,
                LedName = artemisLed.RgbLed.Id.ToString(),
                PhysicalLayout = artemisLed.Device.DeviceType == RGBDeviceType.Keyboard ? (int) artemisLed.Device.PhysicalLayout : null
            };
            LayerEntity.Leds.Add(ledEntity);
        }

        // Adaption hints
        Adapter.Save();

        SaveRenderElement();
    }

    #endregion

    #region Shape management

    private void ShapeTypeOnCurrentValueSet(object? sender, EventArgs e)
    {
        ApplyShapeType();
    }

    private void ApplyShapeType()
    {
        LayerShape = General.ShapeType.CurrentValue switch
        {
            LayerShapeType.Ellipse => new EllipseShape(this),
            LayerShapeType.Rectangle => new RectangleShape(this),
            _ => throw new ArgumentOutOfRangeException()
        };
    }

    #endregion

    #region Rendering

    /// <inheritdoc />
    public override void Update(double deltaTime)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        if (Timeline.IsOverridden)
        {
            Timeline.ClearOverride();
            return;
        }

        try
        {
            UpdateDisplayCondition();
            UpdateTimeline(deltaTime);

            if (ShouldBeEnabled)
                Enable();
            else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any()))
                Disable();

            if (!Enabled || Timeline.Delta == TimeSpan.Zero)
                return;

            General.Update(Timeline);
            Transform.Update(Timeline);
            LayerBrush?.InternalUpdate(Timeline);

            foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
            {
                if (!baseLayerEffect.Suspended)
                    baseLayerEffect.InternalUpdate(Timeline);
            }

            // Remove render copies that finished their timeline and update the rest
            for (int index = 0; index < _renderCopies.Count; index++)
            {
                Layer child = _renderCopies[index];
                if (!child.Timeline.IsFinished)
                {
                    child.Update(deltaTime);
                }
                else
                {
                    _renderCopies.Remove(child);
                    child.Dispose();
                    index--;
                }
            }
        }
        finally
        {
            Timeline.ClearDelta();
        }
    }

    /// <inheritdoc />
    public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        if (editorFocus != null && editorFocus != this)
            return;

        RenderLayer(canvas, basePosition);
        RenderCopies(canvas, basePosition);
    }

    private void RenderLayer(SKCanvas canvas, SKPointI basePosition)
    {
        // Ensure the layer is ready
        if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized || !Leds.Any())
            return;

        // Ensure the brush is ready
        if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false)
            return;

        if (Timeline.IsFinished || LayerBrush?.BrushType != LayerBrushType.Regular)
            return;

        SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
        try
        {
            using SKAutoCanvasRestore _ = new(canvas);
            canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
            using SKPath clipPath = new(Path);
            clipPath.Transform(SKMatrix.CreateTranslation(Bounds.Left * -1, Bounds.Top * -1));
            canvas.ClipPath(clipPath, SKClipOperation.Intersect, true);
            SKRectI layerBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);

            // Apply blend mode and color
            layerPaint.BlendMode = General.BlendMode.CurrentValue;
            layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));

            using SKPath renderPath = new();

            if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle)
                renderPath.AddRect(layerBounds);
            else
                renderPath.AddOval(layerBounds);

            if (General.TransformMode.CurrentValue == LayerTransformMode.Normal)
            {
                // Apply transformation except rotation to the render path
                if (LayerBrush.SupportsTransformation)
                {
                    SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, false);
                    renderPath.Transform(renderPathMatrix);
                }

                // Apply rotation to the canvas
                if (LayerBrush.SupportsTransformation)
                {
                    SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true);
                    canvas.SetMatrix(canvas.TotalMatrix.PreConcat(rotationMatrix));
                }

                DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint);
            }
            else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip)
            {
                SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true);
                renderPath.Transform(renderPathMatrix);

                DelegateRendering(canvas, renderPath, layerBounds, layerPaint);
            }
        }
        finally
        {
            layerPaint.DisposeSelfAndProperties();
        }
    }

    private void RenderCopies(SKCanvas canvas, SKPointI basePosition)
    {
        for (int i = _renderCopies.Count - 1; i >= 0; i--)
            _renderCopies[i].Render(canvas, basePosition, null);
    }

    /// <inheritdoc />
    public override void Enable()
    {
        if (Enabled)
            return;

        bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush");
        if (!tryOrBreak)
            return;

        tryOrBreak = TryOrBreak(() =>
        {
            foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
                baseLayerEffect.InternalEnable();
        }, "Failed to enable one or more effects");
        if (!tryOrBreak)
            return;

        Enabled = true;
    }

    /// <inheritdoc />
    public override void Disable()
    {
        if (!Enabled)
            return;

        LayerBrush?.InternalDisable();
        foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
            baseLayerEffect.InternalDisable();

        Enabled = false;
    }

    /// <inheritdoc />
    public override void OverrideTimelineAndApply(TimeSpan position)
    {
        DisplayCondition.OverrideTimeline(position);

        General.Update(Timeline);
        Transform.Update(Timeline);
        LayerBrush?.InternalUpdate(Timeline);

        foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
            baseLayerEffect.InternalUpdate(Timeline);
    }

    /// <inheritdoc />
    public override void Reset()
    {
        UpdateDisplayCondition();

        if (DisplayConditionMet)
            Timeline.JumpToStart();
        else
            Timeline.JumpToEnd();

        while (_renderCopies.Any())
        {
            _renderCopies[0].Dispose();
            _renderCopies.RemoveAt(0);
        }
    }

    /// <summary>
    ///     Creates a copy of this layer and renders it alongside this layer for as long as its timeline lasts.
    /// </summary>
    /// <param name="max">The total maximum of render copies to keep</param>
    public void CreateRenderCopy(int max)
    {
        if (_renderCopies.Count >= max)
            return;

        Layer copy = new(this);
        _renderCopies.Add(copy);
    }

    internal void CalculateRenderProperties()
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        if (!Leds.Any())
        {
            Path = new SKPath();
        }
        else
        {
            SKPath path = new() {FillType = SKPathFillType.Winding};
            foreach (ArtemisLed artemisLed in Leds)
                path.AddRect(artemisLed.AbsoluteRectangle);

            Path = path;
        }

        // This is called here so that the shape's render properties are up to date when other code
        // responds to OnRenderPropertiesUpdated
        LayerShape?.CalculateRenderProperties();

        // Folder render properties are based on child paths and thus require an update
        if (Parent is Folder folder)
            folder.CalculateRenderProperties();

        OnRenderPropertiesUpdated();
    }

    internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased, SKRect? customBounds = null)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        SKRect bounds = customBounds ?? Bounds;
        SKPoint positionProperty = Transform.Position.CurrentValue;

        // Start at the top left of the shape
        SKPoint position = zeroBased ? new SKPoint(0, 0) : new SKPoint(bounds.Left, bounds.Top);

        // Apply translation
        if (applyTranslation)
        {
            position.X += positionProperty.X * bounds.Width;
            position.Y += positionProperty.Y * bounds.Height;
        }

        return position;
    }

    private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint)
    {
        if (LayerBrush == null)
            throw new ArtemisCoreException("The layer is not yet ready for rendering");

        foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
        {
            if (!baseLayerEffect.Suspended)
                baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint);
        }

        try
        {
            canvas.SaveLayer(layerPaint);
            canvas.ClipPath(renderPath);

            // Restore the blend mode before doing the actual render
            layerPaint.BlendMode = SKBlendMode.SrcOver;

            LayerBrush.InternalRender(canvas, bounds, layerPaint);

            foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
            {
                if (!baseLayerEffect.Suspended)
                    baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint);
            }
        }
        finally
        {
            canvas.Restore();
        }
    }

    /// <summary>
    ///     Creates a transformation matrix that applies the current transformation settings
    /// </summary>
    /// <param name="zeroBased">
    ///     If true, treats the layer as if it is located at 0,0 instead of its actual position on the
    ///     surface
    /// </param>
    /// <param name="includeTranslation">Whether translation should be included</param>
    /// <param name="includeScale">Whether the scale should be included</param>
    /// <param name="includeRotation">Whether the rotation should be included</param>
    /// <param name="customBounds">Optional custom bounds to base the anchor on</param>
    /// <returns>The transformation matrix containing the current transformation settings</returns>
    public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation, SKRect? customBounds = null)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        if (Path == null)
            return SKMatrix.Empty;

        SKRect bounds = customBounds ?? Bounds;
        SKSize sizeProperty = Transform.Scale.CurrentValue;
        float rotationProperty = Transform.Rotation.CurrentValue;

        SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds);
        SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue;

        // Translation originates from the top left of the shape and is tied to the anchor
        float x = anchorPosition.X - (zeroBased ? 0 : bounds.Left) - anchorProperty.X * bounds.Width;
        float y = anchorPosition.Y - (zeroBased ? 0 : bounds.Top) - anchorProperty.Y * bounds.Height;

        SKMatrix transform = SKMatrix.Empty;

        if (includeTranslation)
            // transform is always SKMatrix.Empty here...
            transform = SKMatrix.CreateTranslation(x, y);

        if (includeScale)
        {
            if (transform == SKMatrix.Empty)
                transform = SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y);
            else
                transform = transform.PostConcat(SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y));
        }

        if (includeRotation)
        {
            if (transform == SKMatrix.Empty)
                transform = SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y);
            else
                transform = transform.PostConcat(SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y));
        }

        return transform;
    }

    #endregion

    #region LED management

    /// <summary>
    ///     Adds a new <see cref="ArtemisLed" /> to the layer and updates the render properties.
    /// </summary>
    /// <param name="led">The LED to add</param>
    public void AddLed(ArtemisLed led)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        _leds.Add(led);
        CalculateRenderProperties();
    }

    /// <summary>
    ///     Adds a collection of new <see cref="ArtemisLed" />s to the layer and updates the render properties.
    /// </summary>
    /// <param name="leds">The LEDs to add</param>
    public void AddLeds(IEnumerable<ArtemisLed> leds)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        _leds.AddRange(leds.Except(_leds));
        CalculateRenderProperties();
    }

    /// <summary>
    ///     Removes a <see cref="ArtemisLed" /> from the layer and updates the render properties.
    /// </summary>
    /// <param name="led">The LED to remove</param>
    public void RemoveLed(ArtemisLed led)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        _leds.Remove(led);
        CalculateRenderProperties();
    }

    /// <summary>
    ///     Removes all <see cref="ArtemisLed" />s from the layer and updates the render properties.
    /// </summary>
    public void ClearLeds()
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        _leds.Clear();
        CalculateRenderProperties();
    }

    internal void PopulateLeds(IEnumerable<ArtemisDevice> devices)
    {
        if (Disposed)
            throw new ObjectDisposedException("Layer");

        List<ArtemisLed> leds = new();

        // Get the surface LEDs for this layer
        List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
        foreach (LedEntity ledEntity in LayerEntity.Leds)
        {
            ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier &&
                                                                  a.RgbLed.Id.ToString() == ledEntity.LedName);
            if (match != null)
                leds.Add(match);
            else
                _missingLeds.Add(ledEntity);
        }

        _leds = leds;
        Leds = new ReadOnlyCollection<ArtemisLed>(_leds);
        CalculateRenderProperties();
    }

    private void SaveMissingLeds()
    {
        LayerEntity.Leds.AddRange(_missingLeds.Except(LayerEntity.Leds, LedEntity.LedEntityComparer));
    }

    #endregion

    #region Brush management

    /// <summary>
    ///     Changes the current layer brush to the provided layer brush and activates it
    /// </summary>
    public void ChangeLayerBrush(BaseLayerBrush layerBrush)
    {
        if (layerBrush == null)
            throw new ArgumentNullException(nameof(layerBrush));

        BaseLayerBrush? oldLayerBrush = LayerBrush;
        General.BrushReference.SetCurrentValue(new LayerBrushReference(layerBrush.Descriptor));
        LayerBrush = layerBrush;

        // Don't dispose the brush, only disable it
        // That way an undo-action does not need to worry about disposed brushes
        oldLayerBrush?.InternalDisable();
        ActivateLayerBrush();
    }

    private void ActivateLayerBrush()
    {
        try
        {
            // If the brush is null, try to instantiate it
            if (LayerBrush == null)
            {
                // Ensure the provider is loaded and still provides the expected brush 
                LayerBrushReference? brushReference = General.BrushReference.CurrentValue;
                if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null)
                {
                    // Only apply the brush if it is successfully retrieved from the store and instantiated
                    BaseLayerBrush? layerBrush = LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush);
                    if (layerBrush != null)
                    {
                        ClearBrokenState(BROKEN_STATE_BRUSH_NOT_FOUND);
                        ChangeLayerBrush(layerBrush);
                    }
                    // If that's not possible there's nothing to do
                    else
                    {
                        SetBrokenState(BROKEN_STATE_BRUSH_NOT_FOUND);
                        return;
                    }
                }
            }

            General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
            General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
            Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
            if (LayerBrush != null && Enabled)
            {
                LayerBrush.InternalEnable();
                LayerBrush.Update(0);
            }

            OnLayerBrushUpdated();
            ClearBrokenState(BROKEN_STATE_INIT_FAILED);
        }
        catch (Exception e)
        {
            SetBrokenState(BROKEN_STATE_INIT_FAILED, e);
        }
    }

    private void DeactivateLayerBrush()
    {
        if (LayerBrush == null)
            return;

        BaseLayerBrush? brush = LayerBrush;
        LayerBrush = null;
        brush?.Dispose();

        OnLayerBrushUpdated();
    }

    #endregion
}

/// <summary>
///     Represents a type of layer shape
/// </summary>
public enum LayerShapeType
{
    /// <summary>
    ///     A circular layer shape
    /// </summary>
    Ellipse,

    /// <summary>
    ///     A rectangular layer shape
    /// </summary>
    Rectangle
}

/// <summary>
///     Represents a layer transform mode
/// </summary>
public enum LayerTransformMode
{
    /// <summary>
    ///     Normal transformation
    /// </summary>
    Normal,

    /// <summary>
    ///     Transforms only a clip
    /// </summary>
    Clip
}