using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Pages
{
///
/// ViewModel for the Developer Tools navigation page — exposes a raw KWP/K-Line
/// custom-command console for hardware development and debugging.
///
///
/// Compiled into Debug builds only. The project's
/// DEVELOPER_TOOLS compile-time symbol gates every reference to this
/// type, and the page files are Compile Remove'd from Release builds
/// in HC_APTBS.csproj, so this page does not appear at all in
/// consumer builds.
///
///
public sealed partial class DeveloperPageViewModel : ObservableObject
{
private const string LogId = nameof(DeveloperPageViewModel);
private readonly IKwpService _kwp;
private readonly IAppLogger _log;
/// Root coordinator — exposes K-Line state for status binding.
public MainViewModel Root { get; }
// ── Input / output state ──────────────────────────────────────────────────
///
/// Hex bytes typed by the developer. Whitespace, commas and dashes are
/// accepted as separators; e.g. "18 00 03 FF FF" or "18-00-03-FF-FF".
///
[ObservableProperty] private string _hexInput = string.Empty;
/// Single-line status message (parse error, send result, …).
[ObservableProperty] private string _statusText = string.Empty;
/// True while a send is in flight — disables the Send button.
[ObservableProperty] private bool _isBusy;
/// True when the underlying K-Line session is open.
[ObservableProperty] private bool _isSessionOpen;
/// Time-stamped record of every TX/RX packet exchanged on this page.
public ObservableCollection Log { get; } = new();
// ── Child VMs ─────────────────────────────────────────────────────────────
/// Pump identification card VM, reused from the singleton on .
public ViewModels.PumpIdentificationViewModel Identification => Root.PumpIdentification;
/// ROM / EEPROM dump card.
public DeveloperToolsDumpViewModel Dump { get; }
/// Saved KWP custom commands library.
public DeveloperToolsCommandsViewModel Commands { get; }
/// EEPROM unlock password library.
public DeveloperToolsPasswordsViewModel Passwords { get; }
public DeveloperPageViewModel(
MainViewModel root,
IKwpService kwp,
IConfigurationService config,
IAppLogger log)
{
Root = root;
_kwp = kwp;
_log = log;
IsSessionOpen = root.KLineState == KLineConnectionState.Connected;
root.PropertyChanged += OnRootPropertyChanged;
Dump = new DeveloperToolsDumpViewModel(this, kwp, log);
Commands = new DeveloperToolsCommandsViewModel(this, kwp, config, log);
Passwords = new DeveloperToolsPasswordsViewModel(this, kwp, config, log);
}
private void OnRootPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.KLineState))
{
IsSessionOpen = Root.KLineState == KLineConnectionState.Connected;
SendCommand.NotifyCanExecuteChanged();
}
}
// ── Commands ──────────────────────────────────────────────────────────────
/// Sends the hex payload as a raw KWP custom packet over the persistent session.
[RelayCommand(CanExecute = nameof(CanSend))]
private async Task SendAsync()
{
if (!TryParseHex(HexInput, out var bytes, out var error))
{
StatusText = $"Parse error: {error}";
return;
}
IsBusy = true;
try
{
var hex = FormatHex(bytes);
StatusText = $"Sending {bytes.Length} byte(s)…";
AppendLog(DeveloperLogDirection.Tx, hex);
_log.Info(LogId, $"TX {hex}");
var responses = await _kwp.SendRawCustomAsync(bytes, CancellationToken.None);
if (responses.Count == 0)
{
AppendLog(DeveloperLogDirection.Info, "(no response)");
StatusText = "No response packets.";
return;
}
foreach (var pkt in responses)
{
var rxHex = FormatHex(pkt);
AppendLog(DeveloperLogDirection.Rx, rxHex);
_log.Info(LogId, $"RX {rxHex}");
}
StatusText = $"Received {responses.Count} packet(s).";
}
catch (Exception ex)
{
StatusText = $"Send failed: {ex.Message}";
AppendLog(DeveloperLogDirection.Info, $"ERROR: {ex.Message}");
_log.Warning(LogId, $"SendAsync failed: {ex.Message}");
}
finally
{
IsBusy = false;
}
}
private bool CanSend() => !IsBusy && IsSessionOpen;
/// Clears the on-screen log (does not affect AppLogger output).
[RelayCommand]
private void ClearLog()
{
Log.Clear();
StatusText = string.Empty;
}
// ── Helpers ───────────────────────────────────────────────────────────────
///
/// Appends a row to the shared transaction log. Exposed as internal so
/// the page's child VMs (dump, command library, password library) can stream
/// their own TX/RX rows into the same scrolling log.
///
internal void AppendLog(DeveloperLogDirection dir, string text)
{
// Keep memory bounded — drop oldest entries when the list grows large.
const int max = 500;
if (Log.Count >= max) Log.RemoveAt(0);
Log.Add(new DeveloperLogEntry(DateTime.Now, dir, text));
}
/// Internal accessor so child VMs can push status messages.
internal void SetStatus(string text) => StatusText = text;
///
/// Formats a byte array as a space-separated uppercase hex string. Internal so
/// child VMs can produce log rows that match the parent's formatting.
///
internal static string FormatHex(byte[] bytes) =>
string.Join(" ", bytes.Select(b => b.ToString("X2")));
///
/// Same formatter for any read-only sequence (e.g.
/// returned by KWP read primitives).
///
internal static string FormatHex(System.Collections.Generic.IReadOnlyList bytes)
{
var sb = new System.Text.StringBuilder(bytes.Count * 3);
for (int i = 0; i < bytes.Count; i++)
{
if (i > 0) sb.Append(' ');
sb.Append(bytes[i].ToString("X2"));
}
return sb.ToString();
}
///
/// Parses a hex byte sequence. Accepts spaces, commas, dashes, and any
/// combination as separators. Each token must be 1–2 hex digits.
///
internal static bool TryParseHex(string input, out byte[] bytes, out string error)
{
bytes = Array.Empty();
error = string.Empty;
if (string.IsNullOrWhiteSpace(input))
{
error = "input is empty";
return false;
}
var separators = new[] { ' ', ',', '-', '\t', '\r', '\n' };
var tokens = input.Split(separators, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length == 0)
{
error = "no hex bytes found";
return false;
}
var result = new byte[tokens.Length];
for (int i = 0; i < tokens.Length; i++)
{
var t = tokens[i];
if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t.Substring(2);
if (t.Length is < 1 or > 2)
{
error = $"token #{i + 1} '{tokens[i]}' must be 1–2 hex digits";
return false;
}
if (!byte.TryParse(t, System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out byte b))
{
error = $"token #{i + 1} '{tokens[i]}' is not valid hex";
return false;
}
result[i] = b;
}
bytes = result;
return true;
}
}
/// Log entry direction — TX (sent), RX (received), or Info (status).
public enum DeveloperLogDirection { Tx, Rx, Info }
/// One row in the Developer Tools transaction log.
public sealed record DeveloperLogEntry(DateTime Timestamp, DeveloperLogDirection Direction, string Hex)
{
/// Pre-formatted line text for binding into a single TextBlock.
public string Display
{
get
{
var sb = new StringBuilder();
sb.Append('[').Append(Timestamp.ToString("HH:mm:ss.fff")).Append(']').Append(' ');
sb.Append(Direction switch
{
DeveloperLogDirection.Tx => "TX",
DeveloperLogDirection.Rx => "RX",
_ => " "
});
sb.Append(' ').Append(Hex);
return sb.ToString();
}
}
}
}