import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:dio/dio.dart'; import 'package:flame/components.dart'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import '../config.dart'; /// تصاویرِ کارت از backend می‌آیند (نه bundle داخل اپ) تا حجمِ اپ کم بماند، اما هر /// تصویر فقط **یک‌بار برای همیشه** دانلود می‌شود: بایت‌ها روی دیسکِ ماندگار ذخیره و /// در جلسه‌های بعد از دیسک خوانده می‌شوند (بدونِ شبکه). یک کشِ درون‌حافظه‌ایِ /// همگام ([_ready]) هم نگه می‌داریم تا کارت‌ها بدونِ پرش/انتظار رندر شوند. /// /// مسیرِ سرور: `/cards//.png` و `/cards//back.jpg`. class CardImages { CardImages._(); static const _fallbackSkin = 'simple'; static final Dio _dio = Dio(BaseOptions( responseType: ResponseType.bytes, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 12), validateStatus: (s) => s != null && s >= 200 && s < 300, )); // اسپرایتِ آماده و decode‌شده (کلید: "/") — دسترسیِ همگام برای رندرِ فوری. static final Map _ready = {}; // Futureهای در حالِ اجرا/تمام‌شده برای جلوگیری از بارگذاریِ تکراری. static final Map> _inflight = {}; static Future? _dir; static String _url(String skin, String file) => '${AppConfig.baseUrl}/cards/$skin/$file'; // ===== دسترسیِ همگام (اگر از قبل در حافظه آماده باشد) ===== /// اسپرایتِ رویِ کارت اگر همین حالا در حافظه آماده باشد، وگرنه null. static Sprite? cachedFront(String skin, String code) => _ready['$skin/$code.png']; /// اسپرایتِ پشتِ کارت اگر در حافظه آماده باشد، وگرنه null. static Sprite? cachedBack(String skin) => _ready['$skin/back.jpg']; // ===== بارگذاریِ ناهمگام (دیسک → شبکه) ===== /// اسپرایتِ رویِ یک کارت (مثل "AS")؛ در صورت خطا null (کشیدنِ برداری در CardComponent). static Future front(String skin, String code) => _load(skin, '$code.png'); /// اسپرایتِ پشتِ کارت برای اسکین. static Future back(String skin) => _load(skin, 'back.jpg'); /// کلِ دسته (۵۲ کارت + پشت) را گرم می‌کند: از دیسک (فوری) یا در اولین بار از شبکه. /// fire-and-forget؛ در طولِ صفحه‌ی «جستجوی حریف» اجرا می‌شود تا هنگامِ پخش آماده باشند. static void preloadDeck(String skin) { const ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; const suits = ['S', 'H', 'D', 'C']; for (final s in suits) { for (final r in ranks) { _load(skin, '$r$s.png'); } } _load(skin, 'back.jpg'); } static Future _load(String skin, String file) { final key = '$skin/$file'; final ready = _ready[key]; if (ready != null) return Future.value(ready); return _inflight.putIfAbsent(key, () => _fetch(skin, file)); } static Future _fetch(String skin, String file) async { var s = await _fromDiskOrNet(skin, file); if (s == null && skin != _fallbackSkin) { s = await _fromDiskOrNet(_fallbackSkin, file); } if (s != null) _ready['$skin/$file'] = s; return s; } static Future _fromDiskOrNet(String skin, String file) async { // ۱) دیسکِ ماندگار (اگر قبلاً دانلود شده). final disk = await _readDisk(skin, file); if (disk != null) { final sp = await _toSprite(disk); if (sp != null) return sp; } // ۲) شبکه — سپس ذخیره روی دیسک برای دفعه‌ی بعد. try { final res = await _dio.get>(_url(skin, file)); final data = res.data; if (data == null || data.isEmpty) return null; final bytes = Uint8List.fromList(data); unawaited(_writeDisk(skin, file, bytes)); return _toSprite(bytes); } catch (e) { debugPrint('CardImages: $skin/$file failed: $e'); return null; } } /// تصویرِ فرش از URL کاملِ backend (کش‌شده در حافظه با کلیدِ URL). static Future carpetByUrl(String url) { if (url.isEmpty) return Future.value(null); final key = 'carpet/$url'; final ready = _ready[key]; if (ready != null) return Future.value(ready); return _inflight.putIfAbsent(key, () async { try { final res = await _dio.get>(url); final data = res.data; if (data == null || data.isEmpty) return null; final sp = await _toSprite(Uint8List.fromList(data)); if (sp != null) _ready[key] = sp; return sp; } catch (e) { debugPrint('CardImages: carpet $url failed: $e'); return null; } }); } // ===== دیسک ===== static Future _cacheDir() => _dir ??= getApplicationSupportDirectory().then((d) => '${d.path}/hakem_cards'); static Future _readDisk(String skin, String file) async { try { final f = File('${await _cacheDir()}/$skin/$file'); if (await f.exists()) return f.readAsBytes(); } catch (_) {} return null; } static Future _writeDisk(String skin, String file, Uint8List bytes) async { try { final f = File('${await _cacheDir()}/$skin/$file'); await f.parent.create(recursive: true); await f.writeAsBytes(bytes, flush: false); } catch (_) {} } static Future _toSprite(Uint8List bytes) async { try { final codec = await ui.instantiateImageCodec(bytes); final frame = await codec.getNextFrame(); return Sprite(frame.image); } catch (_) { return null; } } }