// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedMember.Global
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
namespace RGB.NET.Core;
///
///
///
/// Represents a RGB-surface containing multiple devices.
///
public sealed class RGBSurface : AbstractBindable, IDisposable
{
#region Properties & Fields
private readonly Stopwatch _deltaTimeCounter;
private readonly IList _devices = [];
private readonly IList _updateTriggers = [];
private readonly List _ledGroups = [];
///
/// Gets a readonly list containing all loaded .
/// This collection should be locked when enumerated in a multi-threaded application.
///
public IReadOnlyList Devices { get; }
///
/// Gets a readonly list containing all registered .
/// This collection should be locked when enumerated in a multi-threaded application.
///
public IReadOnlyList UpdateTriggers { get; }
///
/// Gets a copy of the representing this .
///
public Rectangle Boundary { get; private set; } = new(new Point(0, 0), new Size(0, 0));
///
/// Gets a list of all on this .
///
public IEnumerable Leds
{
get
{
lock (Devices)
return _devices.SelectMany(x => x);
}
}
#endregion
#region EventHandler
///
/// Represents the event-handler of the -event.
///
/// The arguments provided by the event.
public delegate void ExceptionEventHandler(ExceptionEventArgs args);
///
/// Represents the event-handler of the -event.
///
/// The arguments provided by the event.
public delegate void UpdatingEventHandler(UpdatingEventArgs args);
///
/// Represents the event-handler of the -event.
///
/// The arguments provided by the event.
public delegate void UpdatedEventHandler(UpdatedEventArgs args);
///
/// Represents the event-handler of the -event.
///
/// The arguments provided by the event.
public delegate void SurfaceLayoutChangedEventHandler(SurfaceLayoutChangedEventArgs args);
#endregion
#region Events
// ReSharper disable EventNeverSubscribedTo.Global
///
/// Occurs when a catched exception is thrown inside the .
///
public event ExceptionEventHandler? Exception;
///
/// Occurs when the starts updating.
///
public event UpdatingEventHandler? Updating;
///
/// Occurs when the update is done.
///
public event UpdatedEventHandler? Updated;
///
/// Occurs when the layout of this changed.
///
public event SurfaceLayoutChangedEventHandler? SurfaceLayoutChanged;
// ReSharper restore EventNeverSubscribedTo.Global
#endregion
#region Constructors
///
/// Initializes a new instance of the class.
///
public RGBSurface()
{
_deltaTimeCounter = Stopwatch.StartNew();
Devices = new ReadOnlyCollection(_devices);
UpdateTriggers = new ReadOnlyCollection(_updateTriggers);
}
#endregion
#region Methods
///
/// Perform a full update for all devices. Updates only dirty by default, or all , if flushLeds is set to true.
///
/// Specifies whether all , (including clean ones) should be updated.
//public void Update(bool flushLeds = false) => Update(null, new CustomUpdateData((CustomUpdateDataIndex.FLUSH_LEDS, flushLeds)));
public void Update(bool flushLeds = false) => Update(null, flushLeds ? DefaultCustomUpdateData.FLUSH : DefaultCustomUpdateData.NO_FLUSH);
private void Update(object? updateTrigger, ICustomUpdateData customData) => Update(updateTrigger as IUpdateTrigger, customData);
private void Update(IUpdateTrigger? updateTrigger, ICustomUpdateData customData)
{
try
{
bool flushLeds = customData[CustomUpdateDataIndex.FLUSH_LEDS] as bool? ?? false;
bool render = customData[CustomUpdateDataIndex.RENDER] as bool? ?? true;
bool updateDevices = customData[CustomUpdateDataIndex.UPDATE_DEVICES] as bool? ?? true;
lock (UpdateTriggers)
lock (Devices)
{
OnUpdating(updateTrigger, customData);
// ReSharper disable ForCanBeConvertedToForeach - 'for' has a performance benefit (no enumerator allocation) here and since 'Update' is considered a hot path it's optimized
if (render)
lock (_ledGroups)
{
// Render brushes
for (int i = 0; i < _ledGroups.Count; i++)
{
try { Render(_ledGroups[i]); }
catch (Exception ex) { OnException(ex); }
}
}
if (updateDevices)
for (int i = 0; i < _devices.Count; i++)
{
try { _devices[i].Update(flushLeds); }
catch (Exception ex) { OnException(ex); }
}
// ReSharper restore ForCanBeConvertedToForeach
OnUpdated();
}
}
catch (Exception ex)
{
OnException(ex);
}
}
///
public void Dispose()
{
List devices;
lock (Devices)
devices = [.._devices];
foreach (IRGBDevice device in devices)
try { Detach(device); }
catch { /* We do what we can */}
foreach (IUpdateTrigger updateTrigger in _updateTriggers)
try { updateTrigger.Dispose(); }
catch { /* We do what we can */}
_ledGroups.Clear();
}
///
/// Renders a ledgroup.
///
/// The led group to render.
/// Thrown if the of the Brush is not valid.
private void Render(ILedGroup ledGroup)
{
IBrush? brush = ledGroup.Brush;
if ((brush == null) || !brush.IsEnabled) return;
using (ledGroup.ToListUnsafe(out IList leds))
{
IEnumerable<(RenderTarget renderTarget, Color color)> render;
switch (brush.CalculationMode)
{
case RenderMode.Relative:
Rectangle brushRectangle = new(leds);
Point offset = new(-brushRectangle.Location.X, -brushRectangle.Location.Y);
brushRectangle = brushRectangle.SetLocation(new Point(0, 0));
render = brush.Render(brushRectangle, leds.Select(led => new RenderTarget(led, led.AbsoluteBoundary.Translate(offset))));
break;
case RenderMode.Absolute:
render = brush.Render(Boundary, leds.Select(led => new RenderTarget(led, led.AbsoluteBoundary)));
break;
default:
throw new ArgumentException($"The CalculationMode '{brush.CalculationMode}' is not valid.");
}
foreach ((RenderTarget renderTarget, Color c) in render)
renderTarget.Led.Color = c;
}
}
///
/// Attaches the specified .
///
/// The to attach.
/// true if the could be attached; otherwise, false.
public bool Attach(ILedGroup ledGroup)
{
lock (_ledGroups)
{
if (ledGroup.Surface != null) return false;
ledGroup.Surface = this;
_ledGroups.Add(ledGroup);
_ledGroups.Sort((group1, group2) => group1.ZIndex.CompareTo(group2.ZIndex));
ledGroup.OnAttach();
return true;
}
}
///
/// Detaches the specified .
///
/// The to detache.
/// true if the could be detached; false otherwise.
public bool Detach(ILedGroup ledGroup)
{
lock (_ledGroups)
{
if (!_ledGroups.Remove(ledGroup)) return false;
ledGroup.OnDetach();
ledGroup.Surface = null;
return true;
}
}
///
/// Attaches the specified .
///
/// The to attach.
public void Attach(IRGBDevice device)
{
lock (Devices)
{
if (string.IsNullOrWhiteSpace(device.DeviceInfo.DeviceName)) throw new RGBDeviceException($"The device '{device.DeviceInfo.Manufacturer} {device.DeviceInfo.Model}' has no valid name.");
if (device.Surface != null) throw new RGBSurfaceException($"The device '{device.DeviceInfo.DeviceName}' is already attached to a surface.");
device.Surface = this;
device.BoundaryChanged += DeviceOnBoundaryChanged;
_devices.Add(device);
OnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs.FromAddedDevice(device));
}
}
///
/// Detaches the specified .
///
/// The to detache.
/// true if the could be detached; false otherwise.
public void Detach(IRGBDevice device)
{
lock (Devices)
{
if (!_devices.Contains(device)) throw new RGBSurfaceException($"The device '{device.DeviceInfo.DeviceName}' is not attached to this surface.");
device.BoundaryChanged -= DeviceOnBoundaryChanged;
device.Surface = null;
_devices.Remove(device);
OnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs.FromRemovedDevice(device));
}
}
// ReSharper restore UnusedMember.Global
private void DeviceOnBoundaryChanged(object? sender, EventArgs args)
=> OnSurfaceLayoutChanged((sender is IRGBDevice device) ? SurfaceLayoutChangedEventArgs.FromChangedDevice(device) : SurfaceLayoutChangedEventArgs.Misc());
private void OnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs args)
{
UpdateSurfaceRectangle();
SurfaceLayoutChanged?.Invoke(args);
}
private void UpdateSurfaceRectangle()
{
lock (Devices)
{
Rectangle devicesRectangle = new(_devices.Select(d => d.Boundary));
Boundary = Boundary.SetSize(new Size(devicesRectangle.Location.X + devicesRectangle.Size.Width, devicesRectangle.Location.Y + devicesRectangle.Size.Height));
}
}
///
/// Registers the provided .
///
/// The to register.
public void RegisterUpdateTrigger(IUpdateTrigger updateTrigger)
{
if (!_updateTriggers.Contains(updateTrigger))
{
_updateTriggers.Add(updateTrigger);
updateTrigger.Update += Update;
}
}
///
/// Unregisters the provided .
///
/// The to unregister.
public void UnregisterUpdateTrigger(IUpdateTrigger updateTrigger)
{
if (_updateTriggers.Remove(updateTrigger))
updateTrigger.Update -= Update;
}
///
/// Handles the needed event-calls for an exception.
///
/// The exception previously thrown.
private void OnException(Exception ex)
{
try
{
Exception?.Invoke(new ExceptionEventArgs(ex));
}
catch { /* Well ... that's not my fault */ }
}
///
/// Handles the needed event-calls before updating.
///
private void OnUpdating(IUpdateTrigger? trigger, ICustomUpdateData customData)
{
try
{
double deltaTime = _deltaTimeCounter.Elapsed.TotalSeconds;
_deltaTimeCounter.Restart();
Updating?.Invoke(new UpdatingEventArgs(deltaTime, trigger, customData));
}
catch { /* Well ... that's not my fault */ }
}
///
/// Handles the needed event-calls after an update.
///
private void OnUpdated()
{
try
{
Updated?.Invoke(new UpdatedEventArgs());
}
catch { /* Well ... that's not my fault */ }
}
#endregion
}