// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using ManagedCommon; using PowerDisplay.Common.Drivers; using PowerDisplay.Common.Drivers.DDC; using PowerDisplay.Common.Drivers.WMI; using PowerDisplay.Common.Interfaces; using PowerDisplay.Common.Models; using PowerDisplay.Common.Services; using PowerDisplay.Common.Utils; using Monitor = PowerDisplay.Common.Models.Monitor; namespace PowerDisplay.Helpers { /// /// Monitor manager for unified control of all monitors /// No interface abstraction - KISS principle (only one implementation needed) /// public partial class MonitorManager : IDisposable { private readonly List _monitors = new(); private readonly Dictionary _monitorLookup = new(); private readonly SemaphoreSlim _discoveryLock = new(1, 1); private readonly DisplayRotationService _rotationService = new(); // Controllers stored by type for O(1) lookup based on CommunicationMethod private DdcCiController? _ddcController; private WmiController? _wmiController; private bool _disposed; public IReadOnlyList Monitors => _monitors.AsReadOnly(); public MonitorManager() { // Initialize controllers InitializeControllers(); } /// /// Initialize controllers /// private void InitializeControllers() { try { // DDC/CI controller (external monitors) _ddcController = new DdcCiController(); } catch (Exception ex) { Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}"); } try { // WMI controller (internal monitors) // First check if WMI is available if (WmiController.IsWmiAvailable()) { _wmiController = new WmiController(); } else { Logger.LogWarning("WMI brightness control not available on this system"); } } catch (Exception ex) { Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}"); } } /// /// Discover all monitors from all controllers. /// Each controller is responsible for fully initializing its monitors /// (including brightness, capabilities, input source, color temperature, etc.) /// public async Task> DiscoverMonitorsAsync(CancellationToken cancellationToken = default) { await _discoveryLock.WaitAsync(cancellationToken); try { var discoveredMonitors = await DiscoverFromAllControllersAsync(cancellationToken); // Update collections _monitors.Clear(); _monitorLookup.Clear(); var sortedMonitors = discoveredMonitors .OrderBy(m => m.MonitorNumber) .ToList(); _monitors.AddRange(sortedMonitors); foreach (var monitor in sortedMonitors) { _monitorLookup[monitor.Id] = monitor; } return _monitors.AsReadOnly(); } finally { _discoveryLock.Release(); } } /// /// Discover monitors from all registered controllers in parallel. /// private async Task> DiscoverFromAllControllersAsync(CancellationToken cancellationToken) { var tasks = new List>>(); if (_ddcController != null) { tasks.Add(SafeDiscoverAsync(_ddcController, cancellationToken)); } if (_wmiController != null) { tasks.Add(SafeDiscoverAsync(_wmiController, cancellationToken)); } var results = await Task.WhenAll(tasks); return results.SelectMany(m => m).ToList(); } /// /// Safely discover monitors from a controller, returning empty list on failure. /// private static async Task> SafeDiscoverAsync( IMonitorController controller, CancellationToken cancellationToken) { try { return await controller.DiscoverMonitorsAsync(cancellationToken); } catch (Exception ex) { Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}"); return Enumerable.Empty(); } } /// /// Get brightness of the specified monitor /// public async Task GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default) { var monitor = GetMonitor(monitorId); if (monitor == null) { return VcpFeatureValue.Invalid; } var controller = GetControllerForMonitor(monitor); if (controller == null) { return VcpFeatureValue.Invalid; } try { var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken); // Update cached brightness value if (brightnessInfo.IsValid) { monitor.UpdateStatus(brightnessInfo.ToPercentage(), true); } return brightnessInfo; } catch (Exception ex) { // Mark monitor as unavailable Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}"); monitor.IsAvailable = false; return VcpFeatureValue.Invalid; } } /// /// Set brightness of the specified monitor /// public Task SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default) => ExecuteMonitorOperationAsync( monitorId, brightness, (ctrl, mon, val, ct) => ctrl.SetBrightnessAsync(mon, val, ct), (mon, val) => mon.UpdateStatus(val, true), cancellationToken); /// /// Set contrast of the specified monitor /// public Task SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default) => ExecuteMonitorOperationAsync( monitorId, contrast, (ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct), (mon, val) => mon.CurrentContrast = val, cancellationToken); /// /// Set volume of the specified monitor /// public Task SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default) => ExecuteMonitorOperationAsync( monitorId, volume, (ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct), (mon, val) => mon.CurrentVolume = val, cancellationToken); /// /// Get monitor color temperature /// public async Task GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default) { var monitor = GetMonitor(monitorId); if (monitor == null) { return VcpFeatureValue.Invalid; } var controller = GetControllerForMonitor(monitor); if (controller == null) { return VcpFeatureValue.Invalid; } try { return await controller.GetColorTemperatureAsync(monitor, cancellationToken); } catch (Exception ex) when (ex is not OutOfMemoryException) { Logger.LogDebug($"GetColorTemperatureAsync failed: {ex.Message}"); return VcpFeatureValue.Invalid; } } /// /// Set monitor color temperature /// public Task SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default) => ExecuteMonitorOperationAsync( monitorId, colorTemperature, (ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct), (mon, val) => mon.CurrentColorTemperature = val, cancellationToken); /// /// Get current input source for a monitor /// public async Task GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default) { var monitor = GetMonitor(monitorId); if (monitor == null) { return VcpFeatureValue.Invalid; } var controller = GetControllerForMonitor(monitor); if (controller == null) { return VcpFeatureValue.Invalid; } try { return await controller.GetInputSourceAsync(monitor, cancellationToken); } catch (Exception ex) when (ex is not OutOfMemoryException) { Logger.LogDebug($"GetInputSourceAsync failed: {ex.Message}"); return VcpFeatureValue.Invalid; } } /// /// Set input source for a monitor /// public Task SetInputSourceAsync(string monitorId, int inputSource, CancellationToken cancellationToken = default) => ExecuteMonitorOperationAsync( monitorId, inputSource, (ctrl, mon, val, ct) => ctrl.SetInputSourceAsync(mon, val, ct), (mon, val) => mon.CurrentInputSource = val, cancellationToken); /// /// Set rotation/orientation for a monitor. /// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI). /// /// Monitor ID /// Orientation: 0=normal, 1=90°, 2=180°, 3=270° /// Cancellation token /// Operation result public Task SetRotationAsync(string monitorId, int orientation, CancellationToken cancellationToken = default) { var monitor = GetMonitor(monitorId); if (monitor == null) { Logger.LogError($"[MonitorManager] SetRotation: Monitor not found: {monitorId}"); return Task.FromResult(MonitorOperationResult.Failure("Monitor not found")); } // Rotation uses Windows display settings API, not DDC/CI controller // Prefer using Monitor object which contains GdiDeviceName for accurate adapter targeting var result = _rotationService.SetRotation(monitor, orientation); if (result.IsSuccess) { monitor.Orientation = orientation; monitor.LastUpdate = DateTime.Now; Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}"); } else { Logger.LogError($"[MonitorManager] SetRotation: Failed for {monitorId}: {result.ErrorMessage}"); } return Task.FromResult(result); } /// /// Get monitor by ID. Uses dictionary lookup for O(1) performance. /// public Monitor? GetMonitor(string monitorId) { return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null; } /// /// Get controller for the monitor based on CommunicationMethod. /// O(1) lookup - no async validation needed since controller type is determined at discovery. /// private IMonitorController? GetControllerForMonitor(Monitor monitor) { return monitor.CommunicationMethod switch { "WMI" => _wmiController, "DDC/CI" => _ddcController, _ => null, }; } /// /// Generic helper to execute monitor operations with common error handling. /// Eliminates code duplication across Set* methods. /// private async Task ExecuteMonitorOperationAsync( string monitorId, T value, Func> operation, Action onSuccess, CancellationToken cancellationToken = default) { var monitor = GetMonitor(monitorId); if (monitor == null) { Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}"); return MonitorOperationResult.Failure("Monitor not found"); } var controller = GetControllerForMonitor(monitor); if (controller == null) { Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}"); return MonitorOperationResult.Failure("No controller available for this monitor"); } try { var result = await operation(controller, monitor, value, cancellationToken); if (result.IsSuccess) { onSuccess(monitor, value); monitor.LastUpdate = DateTime.Now; } else { monitor.IsAvailable = false; } return result; } catch (Exception ex) { monitor.IsAvailable = false; Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}"); return MonitorOperationResult.Failure($"Exception: {ex.Message}"); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed && disposing) { _discoveryLock?.Dispose(); // Release controllers _ddcController?.Dispose(); _wmiController?.Dispose(); _monitors.Clear(); _monitorLookup.Clear(); _disposed = true; } } } }