import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum DistanceUnit { milesDecimal, milesChains, kilometers, } extension DistanceUnitLabels on DistanceUnit { String get label { switch (this) { case DistanceUnit.milesDecimal: return 'Miles (decimal)'; case DistanceUnit.milesChains: return 'Miles & chains'; case DistanceUnit.kilometers: return 'Kilometers'; } } String get shortLabel { switch (this) { case DistanceUnit.milesDecimal: return 'mi'; case DistanceUnit.milesChains: return 'm.ch'; case DistanceUnit.kilometers: return 'km'; } } String get _prefsValue => toString().split('.').last; static DistanceUnit fromPrefs(String raw) { return DistanceUnit.values.firstWhere( (u) => u._prefsValue == raw, orElse: () => DistanceUnit.milesDecimal, ); } } class DistanceUnitService extends ChangeNotifier { static const _prefsKey = 'distance_unit'; static const double kmPerMile = 1.609344; static const double chainsPerMile = 80.0; DistanceUnitService() { _load(); } DistanceUnit _unit = DistanceUnit.milesDecimal; bool _loaded = false; DistanceUnit get unit => _unit; bool get isLoaded => _loaded; Future _load() async { final prefs = await SharedPreferences.getInstance(); final saved = prefs.getString(_prefsKey); if (saved != null && saved.trim().isNotEmpty) { _unit = DistanceUnitLabels.fromPrefs(saved.trim()); } _loaded = true; notifyListeners(); } Future setUnit(DistanceUnit unit) async { _unit = unit; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsKey, unit._prefsValue); notifyListeners(); } double? milesFromInput(String input) => DistanceFormatter(_unit).parseInputMiles(input); String format(double miles, {int decimals = 1, bool includeUnit = true}) => DistanceFormatter(_unit) .format(miles, decimals: decimals, includeUnit: includeUnit); double toDisplay(double miles, {int decimals = 1}) => DistanceFormatter(_unit).convertMiles(miles, decimals: decimals); } class DistanceFormatter { DistanceFormatter(this.unit); final DistanceUnit unit; String format(double miles, {int decimals = 1, bool includeUnit = true}) { decimals = decimals.clamp(1, 2); if (unit == DistanceUnit.milesChains) { // Always show chains with two decimals. decimals = 2; } switch (unit) { case DistanceUnit.milesDecimal: final value = _numberFormat(decimals).format(miles); return includeUnit ? '$value mi' : value; case DistanceUnit.kilometers: final kms = miles * DistanceUnitService.kmPerMile; final value = _numberFormat(decimals).format(kms); return includeUnit ? '$value km' : value; case DistanceUnit.milesChains: final value = _formatMilesChains(miles); return includeUnit ? '$value mi' : value; } } double convertMiles(double miles, {int decimals = 1}) { decimals = decimals.clamp(1, 2); switch (unit) { case DistanceUnit.milesDecimal: return double.parse(miles.toStringAsFixed(decimals)); case DistanceUnit.kilometers: final kms = miles * DistanceUnitService.kmPerMile; return double.parse(kms.toStringAsFixed(decimals)); case DistanceUnit.milesChains: // Return miles again; chains representation handled by format. return double.parse(miles.toStringAsFixed(decimals)); } } double milesFromDisplayValue(double value) { switch (unit) { case DistanceUnit.milesDecimal: return value; case DistanceUnit.kilometers: return value / DistanceUnitService.kmPerMile; case DistanceUnit.milesChains: // Value already represents miles when parsed via parseMilesChains. return value; } } double? parseInputMiles(String input) { final trimmed = input.trim(); if (trimmed.isEmpty) return null; switch (unit) { case DistanceUnit.milesDecimal: return double.tryParse(trimmed.replaceAll(',', '')); case DistanceUnit.kilometers: final km = double.tryParse(trimmed.replaceAll(',', '')); if (km == null) return null; return km / DistanceUnitService.kmPerMile; case DistanceUnit.milesChains: return _parseMilesChains(trimmed); } } NumberFormat _numberFormat(int decimals) { final pattern = decimals == 1 ? '#,##0.0' : '#,##0.00'; return NumberFormat(pattern); } String _formatMilesChains(double miles) { final totalChains = miles * DistanceUnitService.chainsPerMile; var milesPart = totalChains ~/ DistanceUnitService.chainsPerMile; final chainRemainder = totalChains - (milesPart * DistanceUnitService.chainsPerMile); // Always show chains as two digits (00-79), rounded to the nearest chain. var roundedChains = chainRemainder.roundToDouble(); if (roundedChains >= DistanceUnitService.chainsPerMile) { milesPart += 1; roundedChains -= DistanceUnitService.chainsPerMile; } final chainText = NumberFormat('00').format(roundedChains); return '$milesPart.$chainText'; } double? _parseMilesChains(String raw) { final cleaned = raw .toLowerCase() .replaceAll(',', '') .replaceAll('m', '.') .replaceAll('c', '.') .replaceAll(RegExp(r'\s+'), '.') .replaceAll(RegExp(r'\.+'), '.') .trim(); if (cleaned.isEmpty) return null; final parts = cleaned.split('.'); if (parts.isEmpty) return null; final milesPart = int.tryParse(parts[0].isEmpty ? '0' : parts[0]); if (milesPart == null) return null; double chainsPart = 0; if (parts.length >= 2) { final chainRaw = parts .sublist(1) .join() .trim(); if (chainRaw.isNotEmpty) { final parsedChains = double.tryParse(chainRaw); if (parsedChains == null) return null; chainsPart = parsedChains; } } final totalMiles = milesPart + (chainsPart / DistanceUnitService.chainsPerMile); return totalMiles; } }