import 'dart:async'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class ShotRecord { final Offset position; final bool isMake; ShotRecord(this.position, this.isMake); } class PlacarController { final String gameId; final String myTeam; final String opponentTeam; final VoidCallback onUpdate; PlacarController({ required this.gameId, required this.myTeam, required this.opponentTeam, required this.onUpdate }); bool isLoading = true; bool isSaving = false; int myScore = 0; int opponentScore = 0; int myFouls = 0; int opponentFouls = 0; int currentQuarter = 1; int myTimeoutsUsed = 0; int opponentTimeoutsUsed = 0; String? myTeamDbId; String? oppTeamDbId; List myCourt = []; List myBench = []; List oppCourt = []; List oppBench = []; Map playerNumbers = {}; Map> playerStats = {}; Map playerDbIds = {}; bool showMyBench = false; bool showOppBench = false; bool isSelectingShotLocation = false; String? pendingAction; String? pendingPlayer; List matchShots = []; Duration duration = const Duration(minutes: 10); Timer? timer; bool isRunning = false; // --- 🔄 CARREGAMENTO COMPLETO (DADOS REAIS + ESTATÍSTICAS SALVAS) --- Future loadPlayers() async { final supabase = Supabase.instance.client; try { await Future.delayed(const Duration(milliseconds: 1500)); // 1. Limpar estados para evitar duplicação myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear(); playerStats.clear(); playerNumbers.clear(); playerDbIds.clear(); myFouls = 0; opponentFouls = 0; // 2. Buscar dados básicos do JOGO final gameResponse = await supabase.from('games').select().eq('id', gameId).single(); myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0; opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0; int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600; duration = Duration(seconds: totalSeconds); myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0; opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0; currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1; // 3. Buscar os IDs das equipas final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); for (var t in teamsResponse) { if (t['name'] == myTeam) myTeamDbId = t['id']; if (t['name'] == opponentTeam) oppTeamDbId = t['id']; } // 4. Buscar os Jogadores List myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : []; List oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : []; // 5. BUSCAR ESTATÍSTICAS JÁ SALVAS final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId); final Map savedStats = { for (var item in statsResponse) item['member_id'].toString(): item }; // 6. Registar a tua equipa for (int i = 0; i < myPlayers.length; i++) { String dbId = myPlayers[i]['id'].toString(); String name = myPlayers[i]['name'].toString(); _registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5); if (savedStats.containsKey(dbId)) { var s = savedStats[dbId]; playerStats[name] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, }; myFouls += (s['fls'] as int? ?? 0); } } _padTeam(myCourt, myBench, "Jogador", isMyTeam: true); // 7. Registar a equipa adversária for (int i = 0; i < oppPlayers.length; i++) { String dbId = oppPlayers[i]['id'].toString(); String name = oppPlayers[i]['name'].toString(); _registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5); if (savedStats.containsKey(dbId)) { var s = savedStats[dbId]; playerStats[name] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, }; opponentFouls += (s['fls'] as int? ?? 0); } } _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); isLoading = false; onUpdate(); } catch (e) { debugPrint("Erro ao retomar jogo: $e"); _padTeam(myCourt, myBench, "Falha", isMyTeam: true); _padTeam(oppCourt, oppBench, "Falha Opp", isMyTeam: false); isLoading = false; onUpdate(); } } void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) { if (playerNumbers.containsKey(name)) name = "$name (Opp)"; playerNumbers[name] = number; if (dbId != null) playerDbIds[name] = dbId; playerStats[name] = {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}; if (isMyTeam) { if (isCourt) myCourt.add(name); else myBench.add(name); } else { if (isCourt) oppCourt.add(name); else oppBench.add(name); } } void _padTeam(List court, List bench, String prefix, {required bool isMyTeam}) { while (court.length < 5) { _registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true); } } // --- TEMPO E TIMEOUTS --- void toggleTimer(BuildContext context) { if (isRunning) { timer?.cancel(); } else { timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (duration.inSeconds > 0) { duration -= const Duration(seconds: 1); } else { timer.cancel(); isRunning = false; if (currentQuarter < 4) { currentQuarter++; duration = const Duration(minutes: 10); myFouls = 0; opponentFouls = 0; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas resetadas!'), backgroundColor: Colors.blue)); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red)); } } onUpdate(); }); } isRunning = !isRunning; onUpdate(); } void useTimeout(bool isOpponent) { if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; } else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; } isRunning = false; timer?.cancel(); onUpdate(); } String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; // --- LÓGICA DE JOGO & VALIDAÇÃO GEOMÉTRICA DE ZONAS --- void handleActionDrag(BuildContext context, String action, String playerData) { String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[name]!; if (stats["fls"]! >= 5 && action != "sub_foul") { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red)); return; } if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { pendingAction = action; pendingPlayer = playerData; isSelectingShotLocation = true; } else { commitStat(action, playerData); } onUpdate(); } void handleSubbing(BuildContext context, String action, String courtPlayerName, bool isOpponent) { if (action.startsWith("bench_my_") && !isOpponent) { String benchPlayer = action.replaceAll("bench_my_", ""); if (playerStats[benchPlayer]!["fls"]! >= 5) return; int courtIndex = myCourt.indexOf(courtPlayerName); int benchIndex = myBench.indexOf(benchPlayer); myCourt[courtIndex] = benchPlayer; myBench[benchIndex] = courtPlayerName; showMyBench = false; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer'))); } if (action.startsWith("bench_opp_") && isOpponent) { String benchPlayer = action.replaceAll("bench_opp_", ""); if (playerStats[benchPlayer]!["fls"]! >= 5) return; int courtIndex = oppCourt.indexOf(courtPlayerName); int benchIndex = oppBench.indexOf(benchPlayer); oppCourt[courtIndex] = benchPlayer; oppBench[benchIndex] = courtPlayerName; showOppBench = false; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer'))); } onUpdate(); } // AGORA RECEBE CONTEXT E SIZE PARA A MATEMÁTICA void registerShotLocation(BuildContext context, Offset position, Size size) { if (pendingAction == null || pendingPlayer == null) return; bool is3Pt = pendingAction!.contains("_3"); bool is2Pt = pendingAction!.contains("_2"); // Validação if (is3Pt || is2Pt) { bool isValid = _validateShotZone(position, size, is3Pt); if (!isValid) { // Se a validação falhar, fudeo. Bloqueia. ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('🛑 Vai dar merda! Local de lançamento incompatível com a pontuação.'), backgroundColor: Colors.red, duration: Duration(seconds: 2), ) ); return; // Aborta! } } bool isMake = pendingAction!.startsWith("add_pts_"); matchShots.add(ShotRecord(position, isMake)); commitStat(pendingAction!, pendingPlayer!); isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } // A MATEMÁTICA DA ZONA bool _validateShotZone(Offset pos, Size size, bool is3Pt) { double w = size.width; double h = size.height; // Ajusta o 0.12 e 0.88 se os teus cestos na imagem estiverem mais para o lado Offset leftHoop = Offset(w * 0.12, h * 0.5); Offset rightHoop = Offset(w * 0.88, h * 0.5); // O raio da linha de 3 pontos (Brinca com este 0.28 se a área ficar muito grande ou pequena) double threePointRadius = w * 0.28; Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop; double distanceToHoop = (pos - activeHoop).distance; // Zonas de canto (onde a linha de 3 é reta) bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) && (pos.dx < w * 0.20 || pos.dx > w * 0.80); if (is3Pt) { return distanceToHoop >= threePointRadius || isCorner3; } else { return distanceToHoop < threePointRadius && !isCorner3; } } void cancelShotLocation() { isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } void commitStat(String action, String playerData) { bool isOpponent = playerData.startsWith("player_opp_"); String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[name]!; if (action.startsWith("add_pts_")) { int pts = int.parse(action.split("_").last); if (isOpponent) opponentScore += pts; else myScore += pts; stats["pts"] = stats["pts"]! + pts; if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; } } else if (action.startsWith("sub_pts_")) { int pts = int.parse(action.split("_").last); if (isOpponent) { opponentScore = (opponentScore - pts < 0) ? 0 : opponentScore - pts; } else { myScore = (myScore - pts < 0) ? 0 : myScore - pts; } stats["pts"] = (stats["pts"]! - pts < 0) ? 0 : stats["pts"]! - pts; if (pts == 2 || pts == 3) { if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; } } else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; } else if (action == "add_rbs") { stats["rbs"] = stats["rbs"]! + 1; } else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; } else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; } else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; } else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; } else if (action == "add_foul") { stats["fls"] = stats["fls"]! + 1; if (isOpponent) { opponentFouls++; } else { myFouls++; } } else if (action == "sub_foul") { if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1; if (isOpponent) { if (opponentFouls > 0) opponentFouls--; } else { if (myFouls > 0) myFouls--; } } } // --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD --- Future saveGameStats(BuildContext context) async { final supabase = Supabase.instance.client; isSaving = true; onUpdate(); try { await supabase.from('games').update({ 'my_score': myScore, 'opponent_score': opponentScore, 'remaining_seconds': duration.inSeconds, 'my_timeouts': myTimeoutsUsed, 'opp_timeouts': opponentTimeoutsUsed, 'current_quarter': currentQuarter, 'status': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado', }).eq('id', gameId); List> batchStats = []; playerStats.forEach((playerName, stats) { String? memberDbId = playerDbIds[playerName]; if (memberDbId != null && stats.values.any((val) => val > 0)) { bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName); String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!; batchStats.add({ 'game_id': gameId, 'member_id': memberDbId, 'team_id': teamId, 'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], }); } }); await supabase.from('player_stats').delete().eq('game_id', gameId); if (batchStats.isNotEmpty) { await supabase.from('player_stats').insert(batchStats); } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas guardadas com Sucesso!'), backgroundColor: Colors.green)); } } catch (e) { debugPrint("Erro ao gravar estatísticas: $e"); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red)); } } finally { isSaving = false; onUpdate(); } } void dispose() { timer?.cancel(); } }