1import 'dart:convert';
  2
  3import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
  4import 'package:blood_pressure_app/model/export_import/import_field_type.dart';
  5import 'package:blood_pressure_app/model/export_import/record_formatter.dart';
  6import 'package:blood_pressure_app/l10n/app_localizations.dart';
  7import 'package:health_data_store/health_data_store.dart';
  8
  9// TODO: respect preferred Pressure unit
 10
 11/// Converters for [BloodPressureRecord] attributes.
 12class NativeColumn extends ExportColumn {
 13  NativeColumn._create(this._csvTitle, this._restoreableType, this._encode, this._decode);
 14
 15  /// All native columns that exist.
 16  ///
 17  /// They are all part of [ExportImportPreset.bloodPressureApp].
 18  static final List<NativeColumn> allColumns = [
 19    timestampUnixMs,
 20    systolic,
 21    diastolic,
 22    pulse,
 23    notes,
 24    color,
 25    needlePin,
 26    intakes,
 27    bodyweight,
 28  ];
 29
 30  static final NativeColumn timestampUnixMs = NativeColumn._create(
 31    'timestampUnixMs',
 32    RowDataFieldType.timestamp,
 33    (record, _, __, ___) => record.time.millisecondsSinceEpoch.toString(),
 34    (pattern) {
 35      final value = int.tryParse(pattern);
 36      return (value == null) ? null : DateTime.fromMillisecondsSinceEpoch(value);
 37    }
 38  );
 39  static final NativeColumn systolic = NativeColumn._create(
 40    'systolic',
 41    RowDataFieldType.sys,
 42    (record, _, __, ___) => (record.sys?.mmHg).toString(),
 43    int.tryParse,
 44  );
 45  static final NativeColumn diastolic = NativeColumn._create(
 46    'diastolic',
 47    RowDataFieldType.dia,
 48    (record, _, __, ___) => (record.dia?.mmHg).toString(),
 49    int.tryParse,
 50  );
 51  static final NativeColumn pulse = NativeColumn._create(
 52    'pulse',
 53    RowDataFieldType.pul,
 54    (record, _, __, ___) => record.pul.toString(),
 55    int.tryParse,
 56  );
 57  static final NativeColumn notes = NativeColumn._create(
 58    'notes',
 59    RowDataFieldType.notes,
 60    (_, note, __, ___) => note.note ?? '',
 61    (pattern) => pattern,
 62  );
 63  static final NativeColumn color = NativeColumn._create(
 64    'color',
 65    RowDataFieldType.color,
 66    (_, note, __, ___) => note.color?.toString() ?? '',
 67    (pattern) {
 68      final value = int.tryParse(pattern);
 69      return value;
 70    }
 71  );
 72  static final NativeColumn needlePin = NativeColumn._create(
 73    'needlePin',
 74    RowDataFieldType.color,
 75    (_, note, __, ___) => '{"color":${note.color}}',
 76    (pattern) {
 77      try {
 78        final json = jsonDecode(pattern);
 79        if (json is! Map<String, dynamic>) return null;
 80        if (json.containsKey('color')) {
 81          final value = json['color'];
 82          return (value is int)
 83            ? value
 84            : null;
 85        }
 86      } on FormatException {
 87        // ignore
 88      }
 89      return null;
 90    }
 91  );
 92  static final NativeColumn intakes = NativeColumn._create(
 93    'intakes',
 94    RowDataFieldType.intakes,
 95    (_, __, intakes, ___) => intakes
 96      .map((i) => '${i.medicine.designation}(${i.dosis.mg})')
 97      .join('|'),
 98    (String pattern) {
 99      final intakes = [];
100      for (final e in pattern.split('|')) {
101        final es = e.split('(');
102        if (es.length < 2) return null;
103        final [med, dosisStr, ...] = es;
104        final dosis = double.tryParse(dosisStr.replaceAll(')', ''));
105        if (dosis == null) return null;
106        intakes.add((med, dosis));
107      }
108      return intakes;
109    }
110  );
111  static final NativeColumn bodyweight = NativeColumn._create(
112    'bodyweight',
113    RowDataFieldType.weightKg,
114      (_, __, ___, weight) => weight?.kg.toString() ?? '',
115      double.tryParse,
116  );
117  
118  final String _csvTitle;
119  final RowDataFieldType _restoreableType;
120  final String Function(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) _encode;
121  /// Function to attempt decoding.
122  ///
123  /// Must either return null or the type indicated by [_restoreableType].
124  final Object? Function(String pattern) _decode;
125
126  @override
127  String get csvTitle => _csvTitle;
128
129  @override
130  (RowDataFieldType, Object)? decode(String pattern) {
131    final value = _decode(pattern);
132    if (value == null) return null;
133    return (_restoreableType, value);
134  }
135
136  @override
137  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) =>
138    _encode(record, note, intakes, bodyweight);
139
140  @override
141  String? get formatPattern => null;
142
143  @override
144  String get internalIdentifier => 'native.$csvTitle';
145
146  @override
147  RowDataFieldType? get restoreAbleType => _restoreableType;
148
149  @override
150  String userTitle(AppLocalizations localizations) => _restoreableType.localize(localizations);
151
152
153}
154
155/// Useful columns that are present by default and recreatable through a formatPattern.
156class BuildInColumn extends ExportColumn {
157  /// Creates a build in column and adds it to allColumns.
158  BuildInColumn._create(this.internalIdentifier, this.csvTitle, String formatString, this._userTitle)
159      : _formatter = ScriptedFormatter(formatString);
160  
161  static final List<ExportColumn> allColumns = [
162    pulsePressure,
163    formattedTime,
164    mhDate,
165    mhSys,
166    mhDia,
167    mhPul,
168    mhDesc,
169    mhTags,
170    mhWeight,
171    mhOxygen,
172  ];
173  
174  static final pulsePressure = BuildInColumn._create(
175      'buildin.pulsePressure',
176      'pulsePressure',
177      r'{{$SYS-$DIA}}', 
178      (localizations) => localizations.pulsePressure,
179  );
180  static final formattedTime = TimeColumn.explicit(
181      'buildin.formattedTime',
182      'Time',
183      'dd MMM yyyy, HH:mm',
184  );
185
186  // my heart columns
187  static final mhDate = TimeColumn.explicit(
188      'buildin.mhDate',
189      'DATUM',
190      r'yyyy-MM-dd HH:mm:ss',
191      '"My Heart" export time',
192  );
193  static final mhSys = BuildInColumn._create(
194      'buildin.mhSys',
195      'SYSTOLE',
196      r'$SYS',
197      (_) => '"My Heart" export sys',
198  );
199  static final mhDia = BuildInColumn._create(
200      'buildin.mhDia',
201      'DIASTOLE',
202      r'$DIA',
203      (_) => '"My Heart" export dia',
204  );
205  static final mhPul = BuildInColumn._create(
206      'buildin.mhPul',
207      'PULSE',
208      r'$PUL',
209      (_) => '"My Heart" export pul',
210  );
211  static final mhDesc = BuildInColumn._create(
212      'buildin.mhDesc',
213      'Beschreibung',
214      r'null',
215      (_) => '"My Heart" export description',
216  );
217  static final mhTags = BuildInColumn._create(
218      'buildin.mhTags',
219      'Tags',
220      r'',
221      (_) => '"My Heart" export tags',
222  );
223  static final mhWeight = BuildInColumn._create(
224      'buildin.mhWeight',
225      'Gewicht',
226      r'0.0',
227      (_) => '"My Heart" export weight',
228  );
229  static final mhOxygen = BuildInColumn._create(
230      'buildin.mhOxygen',
231      'Sauerstoffsättigung',
232      r'0',
233      (_) => '"My Heart" export oxygen',
234  );
235
236  @override
237  final String internalIdentifier;
238
239  @override
240  final String csvTitle;
241
242  final String Function(AppLocalizations localizations) _userTitle;
243
244  @override
245  String userTitle(AppLocalizations localizations) => _userTitle(localizations);
246
247  final Formatter _formatter;
248
249  @override
250  (RowDataFieldType, dynamic)? decode(String pattern) => _formatter.decode(pattern);
251
252  @override
253  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) =>
254    _formatter.encode(record, note, intakes, bodyweight);
255
256  @override
257  String? get formatPattern => _formatter.formatPattern;
258
259  @override
260  RowDataFieldType? get restoreAbleType => _formatter.restoreAbleType;
261}
262
263/// Class for storing data of user added columns.
264class UserColumn extends ExportColumn {
265  /// Create a object that handles export behavior for data in a column.
266  ///
267  /// [formatter] will be created according to [formatPattern].
268  ///
269  /// [internalIdentifier] is automatically prefixed with 'userColumn.' during
270  /// object creation.
271  UserColumn(String internalIdentifier, this.csvTitle, String formatPattern):
272        formatter = ScriptedFormatter(formatPattern),
273        internalIdentifier = 'userColumn.$internalIdentifier';
274
275  /// UserColumn constructor that keeps the internalIdentifier.
276  UserColumn.explicit(this.internalIdentifier, this.csvTitle, String formatPattern):
277        formatter = ScriptedFormatter(formatPattern);
278
279  /// Unique identifier of userColumn.
280  ///
281  /// Is automatically prefixed with `userColumn.` to avoid name collisions with
282  /// build-ins.
283  @override
284  final String internalIdentifier;
285
286  @override
287  final String csvTitle;
288
289  @override
290  String userTitle(AppLocalizations localizations) => csvTitle;
291
292  /// Converter associated with this column.
293  final Formatter formatter;
294
295  @override
296  (RowDataFieldType, dynamic)? decode(String pattern) => formatter.decode(pattern);
297
298  @override
299  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) =>
300    formatter.encode(record, note, intakes, bodyweight);
301
302  @override
303  String? get formatPattern => formatter.formatPattern;
304
305  @override
306  RowDataFieldType? get restoreAbleType => formatter.restoreAbleType;
307}
308
309/// A measurement formatters that converts the timestamp to a string using ICU
310/// patterns.
311class TimeColumn extends ExportColumn {
312  /// Create a formatter that converts between [String]s and [DateTime]s
313  /// through a format pattern.
314  ///
315  /// [internalIdentifier] is automatically prefixed with 'userColumn.' during
316  /// object creation.
317  TimeColumn(this.csvTitle, this.formatPattern):
318      _localization = null,
319      internalIdentifier = 'timeFormatter.$csvTitle';
320
321  /// UserColumn constructor that does not change the [internalIdentifier].
322  TimeColumn.explicit(this.internalIdentifier, this.csvTitle, this.formatPattern, [this._localization]);
323
324  ScriptedTimeFormatter? _formatter;
325
326  @override
327  final String csvTitle;
328
329  final String? _localization;
330
331  @override
332  (RowDataFieldType, dynamic)? decode(String pattern) {
333    _formatter ??= ScriptedTimeFormatter(formatPattern);
334    return _formatter!.decode(pattern);
335  }
336
337  @override
338  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) {
339    _formatter ??= ScriptedTimeFormatter(formatPattern);
340    return _formatter!.encode(record, note, intakes, bodyweight);
341  }
342
343  @override
344  final String formatPattern;
345
346  /// Unique identifier of userColumn.
347  ///
348  /// Is automatically prefixed with `timeFormatter.` to avoid name collisions
349  /// with build-ins.
350  @override
351  final String internalIdentifier;
352
353  @override
354  RowDataFieldType? get restoreAbleType => RowDataFieldType.timestamp;
355
356  @override
357  String userTitle(AppLocalizations localizations) => _localization ?? csvTitle;
358
359}
360
361/// Interface for converters that allow formatting and provide metadata.
362sealed class ExportColumn implements Formatter {
363  /// Unique internal identifier that is used to identify a column in the app.
364  ///
365  /// A identifier can be any string, but is usually structured with a prefix
366  /// and a name. For example `buildin.sys`, `user.fancyvalue` or
367  /// `convert.myheartsys`. These examples are not guaranteed to be the prefixes
368  /// used in the rest of the app.
369  ///
370  /// It should not be used instead of [csvTitle].
371  String get internalIdentifier; // TODO: why is this needed
372
373  /// Column title in a csv file.
374  ///
375  /// May not contain characters intended for CSV column separation (e.g. `,`).
376  String get csvTitle;
377
378  /// Column title in user facing places that don't require strict rules.
379  ///
380  /// It will be displayed on the exported PDF file or in the column selection.
381  String userTitle(AppLocalizations localizations);
382}