import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:practice_engine/practice_engine.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Service for managing deck storage and retrieval. /// Uses persistent storage via shared_preferences. class DeckStorage { static final DeckStorage _instance = DeckStorage._internal(); factory DeckStorage() => _instance; static const String _decksKey = 'decks'; static const String _deckIdsKey = 'deck_ids'; bool _initialized = false; SharedPreferences? _prefs; Future? _initFuture; DeckStorage._internal(); /// Lazy initialization of SharedPreferences Future _initSharedPreferences() async { if (_initFuture == null) { _initFuture = SharedPreferences.getInstance().then((prefs) { _instance._prefs = prefs; _instance._initialized = true; }); } await _initFuture; } /// Initialize storage Future initialize() async { if (_initialized) return; await _initSharedPreferences(); // Don't automatically create default deck - let users add it manually // This allows them to choose whether to include mock data } /// Ensure storage is initialized Future _ensureInitialized() async { if (!_initialized) { await _initSharedPreferences(); await initialize(); } } /// Convert Deck to JSON Map Map _deckToJson(Deck deck) { return { 'id': deck.id, 'title': deck.title, 'description': deck.description, 'currentAttemptIndex': deck.currentAttemptIndex, 'config': { 'requiredConsecutiveCorrect': deck.config.requiredConsecutiveCorrect, 'defaultAttemptSize': deck.config.defaultAttemptSize, 'priorityIncreaseOnIncorrect': deck.config.priorityIncreaseOnIncorrect, 'priorityDecreaseOnCorrect': deck.config.priorityDecreaseOnCorrect, 'immediateFeedbackEnabled': deck.config.immediateFeedbackEnabled, 'includeKnownInAttempts': deck.config.includeKnownInAttempts, 'shuffleAnswerOrder': deck.config.shuffleAnswerOrder, 'timeLimitSeconds': deck.config.timeLimitSeconds, }, 'questions': deck.questions.map((q) => { 'id': q.id, 'prompt': q.prompt, 'answers': q.answers, 'correctAnswerIndices': q.correctAnswerIndices, 'consecutiveCorrect': q.consecutiveCorrect, 'isKnown': q.isKnown, 'priorityPoints': q.priorityPoints, 'lastAttemptIndex': q.lastAttemptIndex, 'totalCorrectAttempts': q.totalCorrectAttempts, 'totalAttempts': q.totalAttempts, }).toList(), 'attemptHistory': deck.attemptHistory.map((entry) => { 'timestamp': entry.timestamp, 'totalQuestions': entry.totalQuestions, 'correctCount': entry.correctCount, 'percentageCorrect': entry.percentageCorrect, 'timeSpent': entry.timeSpent, 'unknownPercentage': entry.unknownPercentage, }).toList(), 'incompleteAttempt': deck.incompleteAttempt != null ? { 'attemptId': deck.incompleteAttempt!.attemptId, 'questionIds': deck.incompleteAttempt!.questionIds, 'startTime': deck.incompleteAttempt!.startTime, 'currentQuestionIndex': deck.incompleteAttempt!.currentQuestionIndex, 'answers': deck.incompleteAttempt!.answers, 'manualOverrides': deck.incompleteAttempt!.manualOverrides, 'pausedAt': deck.incompleteAttempt!.pausedAt, 'remainingSeconds': deck.incompleteAttempt!.remainingSeconds, } : null, }; } /// Convert JSON Map to Deck Deck _jsonToDeck(Map json) { // Parse config final configJson = json['config'] as Map? ?? {}; final config = DeckConfig( requiredConsecutiveCorrect: configJson['requiredConsecutiveCorrect'] as int? ?? 3, defaultAttemptSize: configJson['defaultAttemptSize'] as int? ?? 10, priorityIncreaseOnIncorrect: configJson['priorityIncreaseOnIncorrect'] as int? ?? 5, priorityDecreaseOnCorrect: configJson['priorityDecreaseOnCorrect'] as int? ?? 2, immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true, includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false, shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true, timeLimitSeconds: configJson['timeLimitSeconds'] as int?, ); // Parse questions final questionsJson = json['questions'] as List? ?? []; final questions = questionsJson.map((qJson) { final questionMap = qJson as Map; List? correctIndices; if (questionMap['correctAnswerIndices'] != null) { final indicesJson = questionMap['correctAnswerIndices'] as List?; correctIndices = indicesJson?.map((e) => e as int).toList(); } else if (questionMap['correctAnswerIndex'] != null) { // Backward compatibility final singleIndex = questionMap['correctAnswerIndex'] as int?; if (singleIndex != null) { correctIndices = [singleIndex]; } } return Question( id: questionMap['id'] as String? ?? '', prompt: questionMap['prompt'] as String? ?? '', answers: (questionMap['answers'] as List?) ?.map((e) => e.toString()) .toList() ?? [], correctAnswerIndices: correctIndices ?? [0], consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0, isKnown: questionMap['isKnown'] as bool? ?? false, priorityPoints: questionMap['priorityPoints'] as int? ?? 0, lastAttemptIndex: questionMap['lastAttemptIndex'] as int? ?? -1, totalCorrectAttempts: questionMap['totalCorrectAttempts'] as int? ?? 0, totalAttempts: questionMap['totalAttempts'] as int? ?? 0, ); }).toList(); // Parse attempt history final historyJson = json['attemptHistory'] as List? ?? []; final attemptHistory = historyJson.map((entryJson) { final entryMap = entryJson as Map; // Support both old and new field names for backward compatibility final correctCount = entryMap['correctCount'] as int? ?? entryMap['correctAnswers'] as int? ?? 0; final totalQuestions = entryMap['totalQuestions'] as int? ?? 0; final timeSpent = entryMap['timeSpent'] as int? ?? (entryMap['timeSpentSeconds'] as int? ?? 0) * 1000; // Convert seconds to milliseconds // Calculate percentageCorrect if not present final percentageCorrect = entryMap['percentageCorrect'] as double? ?? (totalQuestions > 0 ? (correctCount / totalQuestions * 100) : 0.0); final unknownPercentage = entryMap['unknownPercentage'] as double? ?? 0.0; return AttemptHistoryEntry( timestamp: entryMap['timestamp'] as int? ?? 0, totalQuestions: totalQuestions, correctCount: correctCount, percentageCorrect: percentageCorrect, timeSpent: timeSpent, unknownPercentage: unknownPercentage, ); }).toList(); // Parse incomplete attempt IncompleteAttempt? incompleteAttempt; if (json['incompleteAttempt'] != null) { final incompleteJson = json['incompleteAttempt'] as Map; incompleteAttempt = IncompleteAttempt( attemptId: incompleteJson['attemptId'] as String? ?? '', questionIds: (incompleteJson['questionIds'] as List?) ?.map((e) => e.toString()) .toList() ?? [], startTime: incompleteJson['startTime'] as int? ?? 0, currentQuestionIndex: incompleteJson['currentQuestionIndex'] as int? ?? 0, answers: Map.from( incompleteJson['answers'] as Map? ?? {}, ), manualOverrides: Map.from( (incompleteJson['manualOverrides'] as Map?)?.map( (key, value) => MapEntry(key, value as bool), ) ?? {}, ), pausedAt: incompleteJson['pausedAt'] as int? ?? 0, remainingSeconds: incompleteJson['remainingSeconds'] as int?, ); } // Create deck return Deck( id: json['id'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(), title: json['title'] as String? ?? 'Imported Deck', description: json['description'] as String? ?? '', questions: questions, config: config, currentAttemptIndex: json['currentAttemptIndex'] as int? ?? 0, attemptHistory: attemptHistory, incompleteAttempt: incompleteAttempt, ); } /// Get all decks Future> getAllDecks() async { await _ensureInitialized(); final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; final decks = []; for (final id in deckIds) { final deck = await getDeck(id); if (deck != null) { decks.add(deck); } } return decks; } /// Synchronous version for backward compatibility List getAllDecksSync() { if (!_initialized || _prefs == null) { // Trigger async initialization but return empty list for now _ensureInitialized(); return []; } final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; final decks = []; for (final id in deckIds) { final deck = getDeckSync(id); if (deck != null) { decks.add(deck); } } return decks; } /// Get a deck by ID Future getDeck(String id) async { await _ensureInitialized(); final deckJson = _prefs!.getString('$_decksKey:$id'); if (deckJson == null) return null; try { final json = jsonDecode(deckJson) as Map; return _jsonToDeck(json); } catch (e) { debugPrint('Error loading deck $id: $e'); return null; } } /// Synchronous version for backward compatibility Deck? getDeckSync(String id) { if (!_initialized || _prefs == null) { // Trigger async initialization but return null for now _ensureInitialized(); return null; } final deckJson = _prefs!.getString('$_decksKey:$id'); if (deckJson == null) return null; try { final json = jsonDecode(deckJson) as Map; return _jsonToDeck(json); } catch (e) { debugPrint('Error loading deck $id: $e'); return null; } } /// Add or update a deck Future saveDeck(Deck deck) async { await _ensureInitialized(); final deckJson = jsonEncode(_deckToJson(deck)); await _prefs!.setString('$_decksKey:${deck.id}', deckJson); // Update deck IDs list final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; if (!deckIds.contains(deck.id)) { deckIds.add(deck.id); await _prefs!.setStringList(_deckIdsKey, deckIds); } } /// Synchronous version for backward compatibility void saveDeckSync(Deck deck) { if (!_initialized || _prefs == null) { // Queue for async save _ensureInitialized().then((_) => saveDeck(deck)); return; } final deckJson = jsonEncode(_deckToJson(deck)); _prefs!.setString('$_decksKey:${deck.id}', deckJson); // Update deck IDs list final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; if (!deckIds.contains(deck.id)) { deckIds.add(deck.id); _prefs!.setStringList(_deckIdsKey, deckIds); } } /// Delete a deck Future deleteDeck(String id) async { await _ensureInitialized(); await _prefs!.remove('$_decksKey:$id'); // Update deck IDs list final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; deckIds.remove(id); await _prefs!.setStringList(_deckIdsKey, deckIds); } /// Synchronous version for backward compatibility void deleteDeckSync(String id) { if (!_initialized || _prefs == null) { // Queue for async delete _ensureInitialized().then((_) => deleteDeck(id)); return; } _prefs!.remove('$_decksKey:$id'); // Update deck IDs list final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; deckIds.remove(id); _prefs!.setStringList(_deckIdsKey, deckIds); } /// Check if a deck exists Future hasDeck(String id) async { await _ensureInitialized(); return _prefs!.containsKey('$_decksKey:$id'); } /// Synchronous version for backward compatibility bool hasDeckSync(String id) { if (!_initialized || _prefs == null) return false; return _prefs!.containsKey('$_decksKey:$id'); } /// Get the number of decks Future get deckCount async { await _ensureInitialized(); final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; return deckIds.length; } /// Synchronous version for backward compatibility int get deckCountSync { if (!_initialized || _prefs == null) return 0; final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; return deckIds.length; } }