using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Pages
{
/// Memory region selector for the dump card.
public enum DumpRegion
{
/// ROM range 0x0000–0x9FFF, read via KWP ReadRomEeprom (0x03).
Rom,
/// EEPROM range 0x00–0xFF, read via KWP ReadEeprom (0x19).
/// Note: only offsets 0x00–0xBF are valid per 256-byte block.
Eeprom,
}
///
/// Developer Tools — ROM / EEPROM dump card. Iterates a user-supplied address
/// range in 13-byte chunks aligned to 256-byte block boundaries (mirroring the
/// legacy DumpRom / DumpEeprom routines in
/// docs/dump functions.txt) and writes the result to
/// %UserProfile%\.HC_APTBS\dumps\{ident}_{swver1}_{start:X4}-{end:X4}.bin.
///
/// Compiled into Debug builds only — see HC_APTBS.csproj.
///
public sealed partial class DeveloperToolsDumpViewModel : ObservableObject
{
private const int MaxChunk = 13;
private const int BlockSize = 0x0100;
// EEPROM has only 0x00–0xBF readable per 256-byte block via the unauth path;
// the legacy DumpEeprom routine treats this as the "valid bytes per block" cap.
private const int EepromValidBytesPerBlock = 0x00C0;
private readonly DeveloperPageViewModel _parent;
private readonly IKwpService _kwp;
private readonly IAppLogger _log;
private const string LogId = nameof(DeveloperToolsDumpViewModel);
///
/// Threshold above which an in-flight dump shows the prominent overlay banner
/// (instead of just the inline progress bar). 0x0A00 = 2560 bytes; below that,
/// reads complete in a few seconds and don't need the bigger indicator.
///
public const int LargeDumpByteThreshold = 0x0A00;
public DeveloperToolsDumpViewModel(DeveloperPageViewModel parent, IKwpService kwp, IAppLogger log)
{
_parent = parent;
_kwp = kwp;
_log = log;
// Sensible default: full ROM dump.
StartAddressHex = "0000";
EndAddressHex = "9FFF";
}
// ── Inputs ────────────────────────────────────────────────────────────────
/// Region radio: ROM or EEPROM. Updates address-range bounds + defaults.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DumpCommand))]
private DumpRegion _region = DumpRegion.Rom;
/// Start address in hex (no 0x prefix required).
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DumpCommand))]
private string _startAddressHex = string.Empty;
/// End address in hex (inclusive).
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DumpCommand))]
private string _endAddressHex = string.Empty;
/// Per-byte progress, 0..1, for the progress bar.
[ObservableProperty] private double _progress;
/// True while a dump is in flight — disables Dump button.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DumpCommand))]
private bool _isDumping;
///
/// True while a dump is in flight AND the requested range is larger than
/// . Drives the page-level banner overlay
/// so the operator sees clearly that a long dump is running.
///
[ObservableProperty] private bool _isLargeDumpInProgress;
/// Total bytes the current dump intends to read.
[ObservableProperty] private int _totalBytes;
/// Bytes received so far for the current dump.
[ObservableProperty] private int _bytesCollected;
/// Address of the next byte to read (for the banner display).
[ObservableProperty] private int _currentAddress;
/// Status / hint text shown under the inputs.
[ObservableProperty] private string _statusText = string.Empty;
/// True when the ROM region is selected. Two-way bindable for radio buttons.
public bool IsRomSelected
{
get => Region == DumpRegion.Rom;
set { if (value) Region = DumpRegion.Rom; }
}
/// True when the EEPROM region is selected. Two-way bindable for radio buttons.
public bool IsEepromSelected
{
get => Region == DumpRegion.Eeprom;
set { if (value) Region = DumpRegion.Eeprom; }
}
partial void OnRegionChanged(DumpRegion value)
{
// Reset to typical full-range defaults so the user doesn't accidentally
// dump a ROM-sized range out of EEPROM and get NAKs all the way.
switch (value)
{
case DumpRegion.Rom:
StartAddressHex = "0000";
EndAddressHex = "9FFF";
break;
case DumpRegion.Eeprom:
StartAddressHex = "0000";
EndAddressHex = "00BF";
break;
}
OnPropertyChanged(nameof(IsRomSelected));
OnPropertyChanged(nameof(IsEepromSelected));
}
// ── Command ───────────────────────────────────────────────────────────────
///
/// Iterates the selected range in 13-byte chunks aligned to 256-byte blocks
/// and writes the bytes to disk. Filename built from the current pump
/// identifier and SW ver 1 read by the K-Line session.
///
[RelayCommand(CanExecute = nameof(CanDump))]
private async Task DumpAsync()
{
if (!TryParseAddresses(out int startAddr, out int endAddr, out string parseError))
{
StatusText = parseError;
return;
}
var ident = (_parent.Root.PumpIdentification.KlinePumpId ?? string.Empty).Trim();
var swVer1 = (_parent.Root.PumpIdentification.KlineSwVersion1 ?? string.Empty).Trim();
if (ident.Length == 0 || swVer1.Length == 0)
{
StatusText = "Read pump identification first — ident and SW ver 1 are required for the filename.";
return;
}
var dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".HC_APTBS", "dumps");
try { Directory.CreateDirectory(dir); }
catch (Exception ex)
{
StatusText = $"Could not create dump folder: {ex.Message}";
return;
}
var fileName = $"{Sanitize(ident)}_{Sanitize(swVer1)}_{startAddr:X4}-{endAddr:X4}.bin";
var fullPath = Path.Combine(dir, fileName);
int totalBytes = endAddr - startAddr + 1;
TotalBytes = totalBytes;
BytesCollected = 0;
CurrentAddress = startAddr;
IsDumping = true;
IsLargeDumpInProgress = totalBytes > LargeDumpByteThreshold;
Progress = 0;
var buffer = new List(totalBytes);
int collected = 0;
try
{
_parent.AppendLog(DeveloperLogDirection.Info,
$"DUMP {Region} 0x{startAddr:X4}-0x{endAddr:X4} ({totalBytes} bytes) → {fileName}");
int addr = startAddr;
while (addr <= endAddr)
{
CurrentAddress = addr;
int blockBase = addr & 0xFF00;
int offsetInBlock = addr & 0x00FF;
int blockEndAbs;
if (Region == DumpRegion.Eeprom)
{
if (offsetInBlock >= EepromValidBytesPerBlock)
{
// Skip the unreadable tail of this block.
addr = blockBase + BlockSize;
continue;
}
blockEndAbs = blockBase + EepromValidBytesPerBlock - 1;
}
else
{
blockEndAbs = blockBase + BlockSize - 1;
}
int maxReadableAbs = Math.Min(endAddr, blockEndAbs);
while (addr <= maxReadableAbs)
{
int remaining = maxReadableAbs - addr + 1;
byte len = (byte)Math.Min(MaxChunk, remaining);
IReadOnlyList chunk = Region == DumpRegion.Rom
? await _kwp.ReadRomEepromAsync((ushort)addr, len)
: await _kwp.ReadEepromAsync((ushort)addr, len);
if (chunk.Count == 0)
{
StatusText = $"Read failed at 0x{addr:X4} (NAK or no session). Saving partial dump.";
_parent.AppendLog(DeveloperLogDirection.Info,
$"DUMP aborted at 0x{addr:X4} — {collected}/{totalBytes} bytes captured.");
break;
}
buffer.AddRange(chunk);
addr += chunk.Count;
collected += chunk.Count;
BytesCollected = collected;
CurrentAddress = addr;
Progress = (double)collected / totalBytes;
}
if (addr <= endAddr && (addr & 0x00FF) == 0)
{
// Already advanced to the next block — keep going.
continue;
}
if (addr <= endAddr)
{
// We broke out of the inner loop early due to a read failure.
break;
}
}
if (buffer.Count > 0)
{
File.WriteAllBytes(fullPath, buffer.ToArray());
_parent.AppendLog(DeveloperLogDirection.Info,
$"DUMP saved {buffer.Count} byte(s) → {fullPath}");
StatusText = buffer.Count == totalBytes
? $"Dump complete: {buffer.Count} bytes saved."
: $"Partial dump: {buffer.Count}/{totalBytes} bytes saved.";
}
else
{
StatusText = "Dump produced no bytes — nothing was written.";
_parent.AppendLog(DeveloperLogDirection.Info, "DUMP produced no bytes — skipped file write.");
}
}
catch (Exception ex)
{
_log.Error(LogId, $"DumpAsync failed: {ex.Message}");
StatusText = $"Dump failed: {ex.Message}";
_parent.AppendLog(DeveloperLogDirection.Info, $"DUMP error: {ex.Message}");
}
finally
{
IsDumping = false;
IsLargeDumpInProgress = false;
}
// Modal confirmation so the operator can't miss completion of a long dump.
var savedToText = buffer.Count > 0 ? $"\n\nFile: {fullPath}" : string.Empty;
var summary = $"{StatusText}{savedToText}";
var image = buffer.Count == totalBytes
? System.Windows.MessageBoxImage.Information
: System.Windows.MessageBoxImage.Warning;
System.Windows.Application.Current?.Dispatcher.BeginInvoke(() =>
System.Windows.MessageBox.Show(
summary,
"Dump finished",
System.Windows.MessageBoxButton.OK,
image));
}
private bool CanDump()
{
if (IsDumping) return false;
if (string.IsNullOrWhiteSpace(StartAddressHex) || string.IsNullOrWhiteSpace(EndAddressHex)) return false;
return TryParseAddresses(out _, out _, out _);
}
// ── Helpers ───────────────────────────────────────────────────────────────
private bool TryParseAddresses(out int startAddr, out int endAddr, out string error)
{
startAddr = 0;
endAddr = 0;
error = string.Empty;
if (!TryParseHexAddress(StartAddressHex, out startAddr))
{
error = $"Bad start address '{StartAddressHex}'.";
return false;
}
if (!TryParseHexAddress(EndAddressHex, out endAddr))
{
error = $"Bad end address '{EndAddressHex}'.";
return false;
}
if (startAddr > endAddr)
{
error = "Invalid range: start > end.";
return false;
}
int min, max;
switch (Region)
{
case DumpRegion.Rom:
min = 0x0000;
max = 0x9FFF;
break;
case DumpRegion.Eeprom:
min = 0x0000;
max = 0x00FF;
break;
default:
error = "Unknown region.";
return false;
}
if (startAddr < min || endAddr > max)
{
error = $"Range outside {Region} bounds (0x{min:X4}–0x{max:X4}).";
return false;
}
return true;
}
private static bool TryParseHexAddress(string text, out int value)
{
value = 0;
if (string.IsNullOrWhiteSpace(text)) return false;
var t = text.Trim();
if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t.Substring(2);
return int.TryParse(t, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
}
private static string Sanitize(string s)
{
var chars = s.Select(c => char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '-' ? c : '_').ToArray();
return new string(chars);
}
}
}