using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
///
/// ViewModel for the pump identification user control.
///
///
/// Owns the pump selector (dropdown), K-Line ECU read command,
/// and all read-only K-Line fields (DFI, serial number, software versions, etc.).
/// After a successful K-Line read the pump is auto-selected from the database
/// using the ECU's pump identifier — matching the old-source behaviour where
/// OnPumpConnectClick would call LoadPump after reading.
///
///
public sealed partial class PumpIdentificationViewModel : ObservableObject
{
// ── Services ──────────────────────────────────────────────────────────────
private readonly IKwpService _kwp;
private readonly IConfigurationService _config;
private readonly ILocalizationService _loc;
private readonly IAppLogger _log;
private const string LogId = "PumpIdentVM";
// ── Constructor ───────────────────────────────────────────────────────────
/// Initialises the ViewModel with the required services.
public PumpIdentificationViewModel(
IKwpService kwpService,
IConfigurationService configService,
ILocalizationService loc,
IAppLogger logger)
{
_kwp = kwpService;
_config = configService;
_loc = loc;
_log = logger;
// Wire KWP progress events to local properties.
_kwp.ProgressChanged += (pct, msg) => App.Current.Dispatcher.Invoke(() =>
{
ProgressPercent = pct;
ProgressMessage = msg;
});
// Start loading the pump as soon as the identifier is read from ROM,
// before the full K-Line read completes. Use BeginInvoke so the K-Line
// background worker thread returns immediately and the read keeps progressing
// even if the UI thread is momentarily blocked (e.g. modal voltage warning).
_kwp.PumpIdentified += (pumpId) => App.Current.Dispatcher.BeginInvoke(() =>
{
_log.Info(LogId, $"PumpIdentified handler: '{pumpId}'");
KlinePumpId = pumpId;
AutoSelectPumpByKlineId(pumpId);
});
// Track K-Line session state for Disconnect button enable/disable.
_kwp.KLineStateChanged += state =>
App.Current.Dispatcher.Invoke(() => KLineState = state);
}
// ── Pump selection ────────────────────────────────────────────────────────
/// List of available pump IDs loaded from the pump database.
public ObservableCollection PumpIds { get; } = new();
/// Currently selected pump ID in the dropdown.
[ObservableProperty]
private string? _selectedPumpId;
/// Currently loaded pump definition.
[ObservableProperty]
private PumpDefinition? _currentPump;
///
/// Raised when changes so the parent ViewModel
/// can react (e.g. register CAN parameters, update test display).
///
public event Action? PumpChanged;
///
/// Raised at the end of a successful K-Line read with the matched pump ID and
/// the ECU serial number (may be empty if the ECU did not return one). Used by
/// the parent ViewModel to detect a physical pump swap when the model ID is unchanged.
///
public event Action? KlineReadCompleted;
/// Populates the pump ID list from the configuration database.
public void LoadPumpIds()
{
PumpIds.Clear();
foreach (var id in _config.GetPumpIds())
PumpIds.Add(id);
}
partial void OnSelectedPumpIdChanged(string? value)
{
if (string.IsNullOrEmpty(value)) return;
LoadPump(value);
}
private void LoadPump(string pumpId)
{
_log.Info(LogId, $"LoadPump: {pumpId}");
var pump = _config.LoadPump(pumpId);
if (pump == null)
{
_log.Warning(LogId, $"Pump {pumpId} not found in database.");
return;
}
CurrentPump = pump;
PumpChanged?.Invoke(pump);
_log.Info(LogId, $"Loaded pump: {pumpId}");
}
// ── K-Line display properties ─────────────────────────────────────────────
/// K-Line read progress percentage (0–100).
[ObservableProperty] private int _progressPercent;
/// K-Line read progress status message.
[ObservableProperty] private string _progressMessage = string.Empty;
/// True while a K-Line read is in progress.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ReadKlineCommand))]
[NotifyCanExecuteChangedFor(nameof(DisconnectKLineCommand))]
private bool _isReading;
/// Current K-Line session state for Disconnect button enable/disable.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DisconnectKLineCommand))]
private KLineConnectionState _kLineState = KLineConnectionState.Disconnected;
/// DFI calibration angle read from ECU EEPROM.
[ObservableProperty] private string _klineDfi = "-";
/// Pump identifier string read from ECU ROM.
[ObservableProperty] private string _klinePumpId = string.Empty;
/// ECU serial number (EEPROM 0x0080).
[ObservableProperty] private string _klineSerialNumber = string.Empty;
/// Model reference from ECU identification text.
[ObservableProperty] private string _klineModelRef = string.Empty;
/// Data record from ECU identification text.
[ObservableProperty] private string _klineDataRecord = string.Empty;
/// Pump control field from ECU identification text (V2+ pumps).
[ObservableProperty] private string _klinePumpControl = string.Empty;
/// Customer change index read from ECU ROM.
[ObservableProperty] private string _klineModelIndex = string.Empty;
/// Software version 1 from ECU identification text.
[ObservableProperty] private string _klineSwVersion1 = string.Empty;
/// Software version 2 from ECU identification text (V2+ pumps).
[ObservableProperty] private string _klineSwVersion2 = string.Empty;
/// Fault code text returned by the ECU.
[ObservableProperty] private string _klineErrors = string.Empty;
/// Connection error message (empty when OK, auto-collapsed in the UI).
[ObservableProperty] private string _klineConnectError = string.Empty;
// ── Commands ──────────────────────────────────────────────────────────────
/// Reads all pump ECU data over K-Line in a background task.
[RelayCommand(CanExecute = nameof(CanReadKline))]
private async Task ReadKlineAsync()
{
// Always freshly detect the FTDI device — never rely on a stored config value.
// This matches the old source where Config.GetKLinePortName() was called every time.
var port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
App.Current.Dispatcher.Invoke(() =>
KlineConnectError = _loc.GetString("Error.KLineNotFound"));
return;
}
// Use the pump's KWP version if one is selected; default to 0 otherwise.
int kwpVersion = CurrentPump?.KwpVersion ?? 0;
IsReading = true;
try
{
var info = await _kwp.ReadAllInfoAsync(port, kwpVersion);
info.TryGetValue(KlineKeys.Dfi, out string? dfi);
info.TryGetValue(KlineKeys.Errors, out string? errors);
info.TryGetValue(KlineKeys.PumpId, out string? pumpId);
info.TryGetValue(KlineKeys.SerialNumber, out string? serial);
info.TryGetValue(KlineKeys.ModelReference, out string? modelRef);
info.TryGetValue(KlineKeys.ModelIndex, out string? modelIndex);
info.TryGetValue(KlineKeys.SwVersion1, out string? sw1);
info.TryGetValue(KlineKeys.SwVersion2, out string? sw2);
info.TryGetValue(KlineKeys.DataRecord, out string? dataRecord);
info.TryGetValue(KlineKeys.PumpControl, out string? pumpControl);
info.TryGetValue(KlineKeys.ConnectError, out string? connectErr);
info.TryGetValue(KlineKeys.Result, out string? result);
App.Current.Dispatcher.Invoke(() =>
{
KlineDfi = dfi ?? "-";
KlineErrors = errors ?? string.Empty;
// Preserve the value set by the PumpIdentified event if the
// dictionary entry is empty (e.g. read failed after ident).
if (!string.IsNullOrEmpty(pumpId))
KlinePumpId = pumpId;
KlineSerialNumber = serial ?? string.Empty;
KlineModelRef = modelRef ?? string.Empty;
KlineModelIndex = modelIndex ?? string.Empty;
KlineSwVersion1 = sw1 ?? string.Empty;
KlineSwVersion2 = sw2 ?? string.Empty;
KlineDataRecord = dataRecord ?? string.Empty;
KlinePumpControl = pumpControl ?? string.Empty;
KlineConnectError = connectErr ?? string.Empty;
});
// Pump auto-selection by pumpID/alias/substring already happened mid-read
// via the PumpIdentified event. If still unmatched, try ModelReference now
// that the full ECU text has been read (ModelRef arrives later than pumpID).
if (CurrentPump == null && !string.IsNullOrEmpty(modelRef))
App.Current.Dispatcher.Invoke(() => TryAutoSelectByModelRef(modelRef!));
// Attach K-Line info to the (now possibly auto-selected) pump.
if (CurrentPump != null)
CurrentPump.KlineInfo = info;
// Notify parent VM so it can detect physical pump swaps when the model ID is unchanged.
KlineReadCompleted?.Invoke(CurrentPump?.Id ?? string.Empty, serial ?? string.Empty);
}
finally
{
IsReading = false;
}
}
private bool CanReadKline() => !IsReading;
/// Closes the persistent K-Line session.
[RelayCommand(CanExecute = nameof(CanDisconnectKLine))]
private void DisconnectKLine()
{
try
{
_kwp.Disconnect();
}
catch (Exception ex)
{
_log.Error(LogId, $"DisconnectKLine: {ex.Message}");
}
}
private bool CanDisconnectKLine() => !IsReading && KLineState == KLineConnectionState.Connected;
// ── Helpers ───────────────────────────────────────────────────────────────
///
/// Tries to match a K-Line pump identifier to a pump in the database and auto-select it.
/// Resolution order:
///
/// - Exact match — K-Line ID equals a canonical pump ID.
/// - Alias match — K-Line ID is listed under a pump's <Aliases><KlineId> entries.
/// - Substring match — pump ID appears inside the K-Line ident string (legacy fallback for noisy ROM reads).
///
/// ModelReference-based equivalence is handled separately after the full K-Line read completes
/// (see ).
///
private void AutoSelectPumpByKlineId(string klinePumpId)
{
var trimmed = (klinePumpId ?? string.Empty).Trim();
if (trimmed.Length == 0)
{
_log.Warning(LogId, "AutoSelectPumpByKlineId: empty K-Line identifier.");
return;
}
// 1. Direct match — the K-Line ID is itself a pump ID in the database.
foreach (var id in PumpIds)
{
if (string.Equals(id, trimmed, StringComparison.OrdinalIgnoreCase))
{
_log.Info(LogId, $"Auto-selected '{id}' from K-Line id '{trimmed}' (exact match).");
SelectedPumpId = id;
return;
}
}
// 2. Alias match — the K-Line ID is registered as an alias under a canonical pump.
var aliased = _config.FindPumpIdByKlineAlias(trimmed);
if (!string.IsNullOrEmpty(aliased))
{
_log.Info(LogId, $"Auto-selected '{aliased}' from K-Line id '{trimmed}' (alias KlineId).");
SelectedPumpId = aliased;
return;
}
// 3. Substring match — the K-Line ident string may contain the pump ID.
foreach (var id in PumpIds)
{
if (trimmed.Contains(id, StringComparison.OrdinalIgnoreCase))
{
_log.Info(LogId, $"Auto-selected '{id}' from K-Line id '{trimmed}' (substring match).");
SelectedPumpId = id;
return;
}
}
_log.Warning(LogId,
$"K-Line pump ID '{trimmed}' not found in pump database ({PumpIds.Count} candidates).");
}
///
/// Late-stage fallback used when the full K-Line read has completed and no pump was
/// matched by ID/alias/substring. Looks up the canonical pump ID by ModelReference alias
/// (e.g. ME190297C150). No-op if a pump is already selected.
///
private void TryAutoSelectByModelRef(string modelRef)
{
if (CurrentPump != null) return;
var trimmed = (modelRef ?? string.Empty).Trim();
if (trimmed.Length == 0) return;
var canonical = _config.FindPumpIdByModelRef(trimmed);
if (string.IsNullOrEmpty(canonical)) return;
_log.Info(LogId, $"Auto-selected '{canonical}' from ModelRef '{trimmed}' (alias ModelRef).");
SelectedPumpId = canonical;
}
}
}