import 'dart:collection'; import 'dart:typed_data'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; class HiveTileCache { HiveTileCache._(); static const String _boxName = 'map_tile_cache_v1'; static const int _maxEntries = 6000; static const int _maxMemoryEntries = 1024; static const Duration _staleAfter = Duration(days: 14); static const Duration _prefetchCooldown = Duration(seconds: 25); static final Map> _inFlight = >{}; static final LinkedHashMap _memoryCache = LinkedHashMap(); static final Map _recentPrefetches = {}; static Future init() async { if (!Hive.isBoxOpen(_boxName)) { await Hive.openBox(_boxName); } } static Box? get _boxOrNull { try { if (!Hive.isBoxOpen(_boxName)) return null; return Hive.box(_boxName); } catch (_) { return null; } } static Uint8List? peek(String url) { final inMemory = _memoryCache.remove(url); if (inMemory != null) { _memoryCache[url] = inMemory; return inMemory; } final box = _boxOrNull; if (box == null) return null; final now = DateTime.now().millisecondsSinceEpoch; final cached = _freshCachedBytes(box, url, now); if (cached == null || cached.isEmpty) return null; _rememberInMemory(url, cached); return cached; } static Future getOrFetch(String url) async { final immediate = peek(url); if (immediate != null) return immediate; final now = DateTime.now().millisecondsSinceEpoch; final box = _boxOrNull; if (box == null) { final bytes = await _fetchWithoutCache(url); if (bytes != null && bytes.isNotEmpty) { _rememberInMemory(url, bytes); } return bytes; } final bytes = await _fetchAndStore(box, url, now); if (bytes != null && bytes.isNotEmpty) { _rememberInMemory(url, bytes); } return bytes; } static void prefetchUrls(Iterable urls, {int maxCount = 48}) { final box = _boxOrNull; if (box == null) return; final now = DateTime.now().millisecondsSinceEpoch; _recentPrefetches.removeWhere( (_, ts) => now - ts > _prefetchCooldown.inMilliseconds, ); var queued = 0; for (final url in urls) { if (queued >= maxCount) break; if (_memoryCache.containsKey(url)) continue; if (box.containsKey(url)) continue; final lastPrefetchTs = _recentPrefetches[url]; if (lastPrefetchTs != null && now - lastPrefetchTs < _prefetchCooldown.inMilliseconds) { continue; } if (_inFlight.containsKey(url)) continue; _recentPrefetches[url] = now; queued += 1; _fetchAndStore(box, url, now).then((bytes) { if (bytes != null && bytes.isNotEmpty) { _rememberInMemory(url, bytes); } }); } } static Uint8List? _freshCachedBytes(Box box, String url, int now) { final cached = box.get(url); if (cached is! Map) return null; final ts = cached['ts']; final bytes = cached['bytes']; if (ts is int && bytes is Uint8List && now - ts <= _staleAfter.inMilliseconds) { return bytes; } return null; } static Future _fetchAndStore(Box box, String url, int now) { final inFlight = _inFlight[url]; if (inFlight != null) return inFlight; final future = _fetchAndStoreInternal(box, url, now); _inFlight[url] = future; future.whenComplete(() { _inFlight.remove(url); }); return future; } static Future _fetchAndStoreInternal( Box box, String url, int now, ) async { try { final response = await http.get(Uri.parse(url)); if (response.statusCode != 200) return null; final bytes = response.bodyBytes; await box.put(url, {'ts': now, 'bytes': bytes}); _rememberInMemory(url, bytes); await _pruneIfNeeded(box); return bytes; } catch (_) { return null; } } static Future _fetchWithoutCache(String url) async { try { final response = await http.get(Uri.parse(url)); if (response.statusCode != 200) return null; return response.bodyBytes; } catch (_) { return null; } } static Future _pruneIfNeeded(Box box) async { final overflow = box.length - _maxEntries; if (overflow <= 0) return; final entries = >[]; for (final key in box.keys) { entries.add(MapEntry(key, box.get(key))); } entries.sort((a, b) { final ats = (a.value is Map && a.value['ts'] is int) ? a.value['ts'] as int : 0; final bts = (b.value is Map && b.value['ts'] is int) ? b.value['ts'] as int : 0; return ats.compareTo(bts); }); for (var i = 0; i < overflow; i++) { final key = entries[i].key; await box.delete(key); if (key is String) { _memoryCache.remove(key); } } } static void _rememberInMemory(String url, Uint8List bytes) { if (bytes.isEmpty) return; _memoryCache.remove(url); _memoryCache[url] = bytes; while (_memoryCache.length > _maxMemoryEntries) { _memoryCache.remove(_memoryCache.keys.first); } } }