// 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;
}
}
}
}