using TeamsISO.Engine.Controller;
using TeamsISO.Engine.Domain;
using IsoHealthStats = TeamsISO.Engine.Domain.IsoHealthStats;
namespace TeamsISO.App.ViewModels;
///
/// Per-row view model for a participant in the participant list.
/// Wraps a domain and exposes ISO toggle and naming commands.
///
public sealed class ParticipantViewModel : ObservableObject
{
private readonly IIsoController _controller;
private Participant _participant;
private bool _isEnabled;
private bool _isProcessing;
private string _customName;
public ParticipantViewModel(IIsoController controller, Participant participant)
{
_controller = controller;
_participant = participant;
_customName = string.Empty;
ToggleIsoCommand = new AsyncRelayCommand(ToggleIsoAsync, () => !_isProcessing);
}
public Guid Id => _participant.Id;
public string DisplayName => _participant.DisplayName;
public string SourceMachine => _participant.CurrentSource?.MachineName ?? "(disconnected)";
public string SourceFullName => _participant.CurrentSource?.FullName ?? "(disconnected)";
public bool IsOnline => _participant.CurrentSource is not null;
public bool IsEnabled
{
get => _isEnabled;
set => SetField(ref _isEnabled, value);
}
private long _framesIn;
private long _framesOut;
private long _framesDropped;
private string _incomingResolution = "—";
private string _incomingFps = "—";
/// Number of frames the receiver has captured so far.
public long FramesIn { get => _framesIn; set => SetField(ref _framesIn, value); }
/// Number of frames the sender has emitted so far.
public long FramesOut { get => _framesOut; set => SetField(ref _framesOut, value); }
/// Frames dropped by the closest-frame strategy when the receiver outpaces the processor.
public long FramesDropped { get => _framesDropped; set => SetField(ref _framesDropped, value); }
private string _stateLabel = "—";
private string _stateColor = "Wd.Text.Tertiary";
/// Human-readable pipeline state ("Receiving", "Error", "—").
public string StateLabel { get => _stateLabel; set => SetField(ref _stateLabel, value); }
/// Resource key of the brush to color the state pill with.
public string StateColor { get => _stateColor; set => SetField(ref _stateColor, value); }
/// Source resolution as "WxH", or em-dash when no frames have been seen yet.
public string IncomingResolution { get => _incomingResolution; set => SetField(ref _incomingResolution, value); }
///
/// Live incoming framerate as "59.94 fps", or em-dash when fewer than 2 frames
/// have been observed since the pipeline started. Computed in the engine via a
/// 30-frame moving window.
///
public string IncomingFps { get => _incomingFps; set => SetField(ref _incomingFps, value); }
/// Updates the live stats display from a controller-side snapshot.
public void UpdateStats(IsoHealthStats stats)
{
FramesIn = stats.FramesIn;
FramesOut = stats.FramesOut;
FramesDropped = stats.FramesDropped;
IncomingResolution = stats.IncomingWidth > 0 && stats.IncomingHeight > 0
? $"{stats.IncomingWidth}×{stats.IncomingHeight}"
: "—";
IncomingFps = stats.IncomingFps > 0
? $"{stats.IncomingFps:0.0} fps"
: "—";
(StateLabel, StateColor) = stats.State switch
{
IsoState.Receiving => ("LIVE", "Wd.Status.Live"),
IsoState.Sending => ("LIVE", "Wd.Status.Live"),
IsoState.NoSignal => ("NO SIGNAL", "Wd.Status.Warn"),
IsoState.Error => ("ERROR", "Wd.Status.Error"),
IsoState.Idle => (IsEnabled ? "STARTING" : "—", "Wd.Text.Tertiary"),
_ => ("—", "Wd.Text.Tertiary"),
};
}
public bool IsProcessing
{
get => _isProcessing;
private set
{
if (SetField(ref _isProcessing, value))
ToggleIsoCommand.RaiseCanExecuteChanged();
}
}
public string CustomName
{
get => _customName;
set => SetField(ref _customName, value);
}
public AsyncRelayCommand ToggleIsoCommand { get; }
/// Refreshes the underlying participant data (called when the controller emits an updated list).
public void Update(Participant updated)
{
_participant = updated;
OnPropertyChanged(nameof(DisplayName));
OnPropertyChanged(nameof(SourceMachine));
OnPropertyChanged(nameof(SourceFullName));
OnPropertyChanged(nameof(IsOnline));
}
private async Task ToggleIsoAsync()
{
IsProcessing = true;
try
{
if (IsEnabled)
{
await _controller.DisableIsoAsync(Id, CancellationToken.None);
IsEnabled = false;
}
else
{
await _controller.EnableIsoAsync(
Id,
string.IsNullOrWhiteSpace(_customName) ? null : _customName,
CancellationToken.None);
IsEnabled = true;
}
}
finally
{
IsProcessing = false;
}
}
}