using System;
using System.Globalization;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
///
/// ViewModel for the advance monitoring display showing PSG, Injector, and Manual
/// encoder angles with zero-reference tracking and lock angle calculation.
///
public sealed partial class AngleDisplayViewModel : ObservableObject
{
private const string NaN = "- -";
private const double LockAngleTolerance = 0.1;
private readonly int _encoderResolution;
// ── Internal state ────────────────────────────────────────────────────────
private double _psgRaw;
private bool _psgWorking;
private double _injRaw;
private bool _injWorking;
private double _manualRaw;
private double _currentRpm;
private bool _isDirectionRight;
private double _zeroPsgEncoder;
private double _zeroInjDegrees;
private double _injEncoderDegrees;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
[NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))]
private double _currentManualDegrees;
private double _lockAngleValue;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TargetAngleForGauge))]
private bool _isLockSet;
private bool _isManualVisible;
// ── Observable properties (bound by View) ─────────────────────────────────
/// Formatted relative PSG angle or "- -" when sensor offline.
[ObservableProperty] private string _psgRelativeAngle = NaN;
/// Formatted raw PSG encoder degrees or "- -" when sensor offline.
[ObservableProperty] private string _psgEncoderAngle = NaN;
/// Foreground brush for PSG angle text (White = OK, Red = offline).
[ObservableProperty] private Brush _psgAngleForeground = Brushes.White;
/// Formatted relative INJ angle or "- -" when sensor offline.
[ObservableProperty] private string _injRelativeAngle = NaN;
/// Formatted raw INJ encoder degrees or "- -" when sensor offline.
[ObservableProperty] private string _injEncoderAngle = NaN;
/// Foreground brush for INJ angle text (White = OK, Red = offline).
[ObservableProperty] private Brush _injAngleForeground = Brushes.White;
/// Manual encoder angle in degrees, or "-" when RPM >= 30.
[ObservableProperty] private string _manualAngleText = "-";
/// Delta offset input for lock angle calculation (two-way bound to TextBox).
[ObservableProperty] private string _lockAngleDeltaInput = "0";
/// Computed lock angle result display.
[ObservableProperty] private string _lockAngleDisplay = "00.0";
/// Foreground brush for the lock angle display (Green/Red/White).
[ObservableProperty] private Brush _lockAngleForeground = Brushes.White;
// ── Numeric doubles for radial advance-monitor control ────────────────────
/// PSG relative angle as a double for gauge binding.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
private double _psgRelativeDegrees;
/// INJ encoder absolute degrees (0–360) as a double.
[ObservableProperty] private double _injEncoderDegreesValue;
/// Lock angle target (0–360 degrees) as a double for gauge binding.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TargetAngleForGauge))]
private double _targetLockAngle;
/// True when bench RPM ≥ 31, hysteresis-cleared below 29.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
[NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))]
private bool _isRunningMode;
// ── Constructor ───────────────────────────────────────────────────────────
///
/// Creates the angle display ViewModel, reading encoder resolution from configuration.
///
public AngleDisplayViewModel(IConfigurationService configService)
{
_encoderResolution = configService.Settings.EncoderResolution;
}
// ── Computed for radial gauge ─────────────────────────────────────────────
/// Primary thumb angle: manual-wheel angle in hand-mode, PSG relative angle when running.
public double PrimaryGaugeAngle => IsRunningMode ? PsgRelativeDegrees : CurrentManualDegrees;
/// Target thumb angle for the radial gauge, or null when no lock has been set yet.
public double? TargetAngleForGauge => IsLockSet ? TargetLockAngle : (double?)null;
/// Ghost secondary thumb angle: manual-wheel position shown when running, null in hand-mode (primary already shows it).
public double? SecondaryGaugeAngle => IsRunningMode ? CurrentManualDegrees : (double?)null;
// ── Public update (called from MainViewModel.OnRefreshTick) ───────────────
///
/// Feeds all encoder channel values and recalculates display properties.
/// Must be called on the UI thread.
///
public void Update(
double psgRaw, bool psgWorking,
double injRaw, bool injWorking,
double manualRaw, double rpm,
bool isDirectionRight)
{
_psgRaw = psgRaw;
_psgWorking = psgWorking;
_injRaw = injRaw;
_injWorking = injWorking;
_manualRaw = manualRaw;
_currentRpm = rpm;
_isDirectionRight = isDirectionRight;
if (!IsRunningMode && rpm >= 31.0)
IsRunningMode = true;
else if (IsRunningMode && rpm < 29.0)
IsRunningMode = false;
RecalculatePsg();
RecalculateInj();
RecalculateManual();
}
// ── Commands ──────────────────────────────────────────────────────────────
/// Sets the current PSG encoder value as the zero reference.
[RelayCommand]
private void SetPsgZero()
{
_zeroPsgEncoder = _psgRaw;
RecalculatePsg();
}
/// Sets the current INJ encoder degrees as the zero reference.
[RelayCommand]
private void SetInjZero()
{
_zeroInjDegrees = _injEncoderDegrees;
RecalculateInj();
}
// ── Public methods for test phase integration ─────────────────────────────
///
/// Sets the lock angle delta from the pump definition and returns the computed
/// lock angle result. Called when the LockAngleFaseReady event fires.
///
/// Angle offset from .
/// Computed lock angle in degrees (0–360).
public double SetLockAngle(double pumpLockAngle)
{
LockAngleDeltaInput = pumpLockAngle.ToString(CultureInfo.InvariantCulture);
return _lockAngleValue;
}
///
/// Zeros the PSG encoder from a test phase (PsgModeFaseReady event).
///
public void SetPsgZeroFromTest()
{
SetPsgZero();
}
// ── Lock angle delta change handler ───────────────────────────────────────
partial void OnLockAngleDeltaInputChanged(string value)
{
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double delta))
{
_lockAngleValue = CalculateLockAngle(delta);
TargetLockAngle = _lockAngleValue;
IsLockSet = true;
LockAngleDisplay = FormatAngle(_lockAngleValue);
}
RecalculateLockColor();
}
// ── Private recalculation methods ─────────────────────────────────────────
private void RecalculatePsg()
{
if (!_psgWorking)
{
PsgRelativeAngle = NaN;
PsgEncoderAngle = NaN;
PsgAngleForeground = Brushes.Red;
return;
}
PsgAngleForeground = Brushes.White;
double sign = DirectionSign;
// Raw encoder → degrees, normalized to ±180.
double rawDeg = _psgRaw * (360.0 / _encoderResolution);
if (rawDeg > 180) rawDeg = 360 - rawDeg;
rawDeg *= sign;
// Relative angle from zero reference.
double relDeg = (_psgRaw - _zeroPsgEncoder) * (360.0 / _encoderResolution);
if (relDeg > 180) relDeg = 360 - relDeg;
relDeg *= sign;
PsgRelativeDegrees = relDeg;
PsgEncoderAngle = FormatAngle(rawDeg);
PsgRelativeAngle = FormatAngle(relDeg);
}
private void RecalculateInj()
{
if (!_injWorking)
{
InjRelativeAngle = NaN;
InjEncoderAngle = NaN;
InjAngleForeground = Brushes.Red;
return;
}
InjAngleForeground = Brushes.White;
double sign = DirectionSign;
// INJ encoder → degrees (full 0–360, NO 180 normalization).
_injEncoderDegrees = _injRaw * (360.0 / _encoderResolution);
InjEncoderDegreesValue = _injEncoderDegrees;
// Relative angle from zero reference.
double relDeg = (_injEncoderDegrees - _zeroInjDegrees) * sign;
InjEncoderAngle = FormatAngle(_injEncoderDegrees);
InjRelativeAngle = FormatAngle(relDeg);
// Recompute lock angle if already set (INJ encoder changed).
if (_isLockSet &&
double.TryParse(LockAngleDeltaInput, NumberStyles.Float,
CultureInfo.InvariantCulture, out double delta))
{
_lockAngleValue = CalculateLockAngle(delta);
TargetLockAngle = _lockAngleValue;
LockAngleDisplay = FormatAngle(_lockAngleValue);
}
}
private void RecalculateManual()
{
CurrentManualDegrees = _manualRaw * (360.0 / _encoderResolution);
if (_currentRpm < 30)
{
ManualAngleText = FormatAngle(_currentManualDegrees);
_isManualVisible = true;
}
else if (_isManualVisible)
{
ManualAngleText = "-";
_isManualVisible = false;
}
RecalculateLockColor();
}
private void RecalculateLockColor()
{
if (!_isManualVisible)
{
LockAngleForeground = Brushes.White;
return;
}
if (_isLockSet && Math.Abs(_lockAngleValue - _currentManualDegrees) <= LockAngleTolerance)
LockAngleForeground = Brushes.Green;
else if (_isLockSet)
LockAngleForeground = Brushes.Red;
else
LockAngleForeground = Brushes.White;
}
private double CalculateLockAngle(double angleToAdd)
{
double result = (_injEncoderDegrees + angleToAdd) % 360;
if (result < 0) result += 360;
return result;
}
// ── Helpers ───────────────────────────────────────────────────────────────
private double DirectionSign => _isDirectionRight ? -1.0 : 1.0;
private static string FormatAngle(double degrees)
=> degrees.ToString("F1", CultureInfo.InvariantCulture);
}
}