using System; using System.Text.RegularExpressions; using System.Threading; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using HC_APTBS.Services; namespace HC_APTBS.ViewModels.Dialogs { /// /// ViewModel for the Ford VP44 immobilizer unlock progress dialog. /// Tracks Phase 1 (CAN flood ~600 s), Phase 2 (handshake ~4 s), and verification. /// Equivalent to the old WUnlocker window. /// public sealed partial class UnlockProgressViewModel : ObservableObject, IDisposable { private readonly IUnlockService _unlockService; private readonly ILocalizationService _loc; private readonly CancellationTokenSource _cts; /// Regex to extract percentage and elapsed time from Phase 1 status messages. private static readonly Regex ProgressRegex = new(@"Unlocking\.\.\. (\d+)% \((\d{2}:\d{2})\)", RegexOptions.Compiled); /// Creates the ViewModel and subscribes to unlock service events. /// The unlock service to monitor. /// Pump unlock type (1 or 2). /// Cancellation token source to cancel the unlock. public UnlockProgressViewModel(IUnlockService unlockService, int unlockType, CancellationTokenSource cts, ILocalizationService loc) { _unlockService = unlockService; _loc = loc; _cts = cts; _unlockTypeLabel = string.Format(_loc.GetString("Dialog.Unlock.TypeLabel"), unlockType); _phaseText = _loc.GetString("Dialog.Unlock.Phase1"); _elapsedTime = "00:00"; _isCancellable = true; _unlockService.StatusChanged += OnStatusChanged; _unlockService.UnlockCompleted += OnUnlockCompleted; _unlockService.PumpRelocked += OnPumpRelocked; // PumpUnlocked fires as soon as the CAN TestUnlock parameter flips — // regardless of which code path caused the unlock (fast unlock, Phase 1 // flood finishing, external manual unlock). This lets the dialog flip // to its success state the instant the hardware confirms unlock, rather // than waiting for UnlockService.UnlockAsync to reach its final // verification step. _unlockService.PumpUnlocked += OnPumpUnlocked; } // ── Observable properties ──────────────────────────────────────────────── /// Progress percentage (0–100). [ObservableProperty] private int _progress; /// Elapsed time formatted as MM:SS. [ObservableProperty] private string _elapsedTime; /// Current phase description. [ObservableProperty] private string _phaseText; /// Result text shown after completion. [ObservableProperty] private string _resultText = string.Empty; /// Label for unlock type (e.g. "Type 1"). [ObservableProperty] private string _unlockTypeLabel; /// True when the unlock sequence has finished (success, failure, or cancelled). [NotifyCanExecuteChangedFor(nameof(CloseCommand))] [ObservableProperty] private bool _isComplete; /// True while cancellation is allowed (Phase 1 only). [NotifyCanExecuteChangedFor(nameof(CancelCommand))] [ObservableProperty] private bool _isCancellable; /// Tri-state result: null = in progress, true = success, false = failure. [ObservableProperty] private bool? _isSuccess; /// True when the pump is currently LOCKED and the operator can retry the unlock. [NotifyCanExecuteChangedFor(nameof(RetryCommand))] [ObservableProperty] private bool _canRetry; // ── Commands ───────────────────────────────────────────────────────────── /// Cancels the unlock sequence (only available during Phase 1). [RelayCommand(CanExecute = nameof(IsCancellable))] private void Cancel() { _cts.Cancel(); IsCancellable = false; IsComplete = true; IsSuccess = false; ResultText = _loc.GetString("Dialog.Unlock.Cancelled"); } /// Closes the dialog (only available after completion). [RelayCommand(CanExecute = nameof(IsComplete))] private void Close() { RequestClose?.Invoke(); } /// Requests a new unlock attempt (only available when complete and pump is LOCKED). [RelayCommand(CanExecute = nameof(CanRetry))] private void Retry() { CanRetry = false; RequestRetry?.Invoke(); } // ── Events ─────────────────────────────────────────────────────────────── /// Raised when the dialog should close itself. public event Action? RequestClose; /// Raised when the operator presses Retry — parent should restart the unlock sequence. public event Action? RequestRetry; // ── Service event handlers ─────────────────────────────────────────────── private void OnStatusChanged(string msg) { Application.Current?.Dispatcher?.Invoke(() => { var match = ProgressRegex.Match(msg); if (match.Success) { Progress = int.Parse(match.Groups[1].Value); ElapsedTime = match.Groups[2].Value; return; } if (msg == "Fast unlock attempt...") { PhaseText = _loc.GetString("Dialog.Unlock.FastAttempt"); } else if (msg == "Unlocking...") { PhaseText = _loc.GetString("Dialog.Unlock.Phase1"); } else if (msg == "Testing unlock...") { PhaseText = _loc.GetString("Dialog.Unlock.Phase2Testing"); IsCancellable = false; Progress = 100; } else if (msg == "Sending...") { PhaseText = _loc.GetString("Dialog.Unlock.Phase2Sending"); } }); } private void OnUnlockCompleted(bool success) { Application.Current?.Dispatcher?.Invoke(() => { IsComplete = true; IsCancellable = false; IsSuccess = success; ResultText = success ? _loc.GetString("Dialog.Unlock.Unlocked") : _loc.GetString("Dialog.Unlock.Failed"); // Enable Retry when the unlock finished but the pump is still LOCKED. CanRetry = !_unlockService.IsPumpUnlocked; }); } /// /// Observer says the pump is now unlocked. Flip the dialog to the /// success state immediately so the operator sees a responsive UI; the /// later UnlockCompleted(true) event is idempotent and leaves this state /// intact. If UnlockCompleted later arrives with failure=false, that /// would overwrite — but that combination (observer unlocks then service /// reports failure) is not a real scenario in the current state machine. /// private void OnPumpUnlocked() { Application.Current?.Dispatcher?.Invoke(() => { if (IsComplete) return; IsComplete = true; IsCancellable = false; IsSuccess = true; CanRetry = false; ResultText = _loc.GetString("Dialog.Unlock.Unlocked"); }); } /// /// Observer says the pump re-locked after a previously successful unlock. /// If the snackbar is still visible (not dismissed), light up the Retry button /// so the operator has a manual fallback without needing to reselect the pump. /// private void OnPumpRelocked() { Application.Current?.Dispatcher?.Invoke(() => { if (IsComplete) CanRetry = true; }); } // ── IDisposable ────────────────────────────────────────────────────────── /// Unsubscribes from service events to prevent leaks. public void Dispose() { _unlockService.StatusChanged -= OnStatusChanged; _unlockService.UnlockCompleted -= OnUnlockCompleted; _unlockService.PumpUnlocked -= OnPumpUnlocked; _unlockService.PumpRelocked -= OnPumpRelocked; } } }