import 'dart:async'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class ShotRecord { final double relativeX; final double relativeY; final bool isMake; final String playerName; // Bónus: Agora guardamos quem foi o jogador! ShotRecord({ required this.relativeX, required this.relativeY, required this.isMake, required this.playerName }); } 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; // 👇 TRINCO DE SEGURANÇA: Evita contar vitórias duas vezes se clicares no Guardar repetidamente! bool gameWasAlreadyFinished = 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)); myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear(); playerStats.clear(); playerNumbers.clear(); playerDbIds.clear(); myFouls = 0; opponentFouls = 0; 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; // 👇 Verifica se o jogo já tinha acabado noutra sessão gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; 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']; } 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') : []; 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 }; 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, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, }; myFouls += (s['fls'] as int? ?? 0); } } _padTeam(myCourt, myBench, "Jogador", isMyTeam: true); 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, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 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, "ftm": 0, "fta": 0, "orb": 0, "drb": 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); } } 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; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue)); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), 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')}"; 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(); } void registerShotLocation(BuildContext context, Offset position, Size size) { if (pendingAction == null || pendingPlayer == null) return; bool is3Pt = pendingAction!.contains("_3"); bool is2Pt = pendingAction!.contains("_2"); if (is3Pt || is2Pt) { bool isValid = _validateShotZone(position, size, is3Pt); if (!isValid) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Local incompatível com a pontuação.'), backgroundColor: Colors.red, duration: Duration(seconds: 2))); return; } } bool isMake = pendingAction!.startsWith("add_pts_"); // 👇 A MÁGICA DAS COORDENADAS RELATIVAS (0.0 a 1.0) 👇 double relX = position.dx / size.width; double relY = position.dy / size.height; // Extrai só o nome do jogador String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); // Guarda na lista! matchShots.add(ShotRecord( relativeX: relX, relativeY: relY, isMake: isMake, playerName: name )); commitStat(pendingAction!, pendingPlayer!); isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } bool _validateShotZone(Offset pos, Size size, bool is3Pt) { double w = size.width; double h = size.height; Offset leftHoop = Offset(w * 0.12, h * 0.5); Offset rightHoop = Offset(w * 0.88, h * 0.5); double threePointRadius = w * 0.28; Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop; double distanceToHoop = (pos - activeHoop).distance; 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; } if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 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; } if (pts == 1) { if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1; if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1; } } else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; } else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; } else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; 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 { bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0; String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; // 👇👇👇 0. CÉREBRO: CALCULAR OS LÍDERES E MVP DO JOGO 👇👇👇 String topPtsName = '---'; int maxPts = -1; String topAstName = '---'; int maxAst = -1; String topRbsName = '---'; int maxRbs = -1; String topDefName = '---'; int maxDef = -1; String mvpName = '---'; int maxMvpScore = -1; // Passa por todos os jogadores e calcula a matemática playerStats.forEach((playerName, stats) { int pts = stats['pts'] ?? 0; int ast = stats['ast'] ?? 0; int rbs = stats['rbs'] ?? 0; int stl = stats['stl'] ?? 0; int blk = stats['blk'] ?? 0; int defScore = stl + blk; // Defesa: Roubos + Cortes int mvpScore = pts + ast + rbs + defScore; // Impacto Total (MVP) // Compara com o máximo atual e substitui se for maior if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; } if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; } if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; } if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; } if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; } // MVP não leva nº à frente, fica mais limpo }); // 👆👆👆 FIM DO CÉREBRO 👆👆👆 // 1. Atualizar o Jogo na BD (Agora inclui os Reis da partida!) 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': newStatus, // ENVIA A MATEMÁTICA PARA A TUA BASE DE DADOS 'top_pts_name': topPtsName, 'top_ast_name': topAstName, 'top_rbs_name': topRbsName, 'top_def_name': topDefName, 'mvp_name': mvpName, }).eq('id', gameId); // 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) { final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]); Map myTeamUpdate = {}; Map oppTeamUpdate = {}; for(var t in teamsData) { if(t['id'].toString() == myTeamDbId) myTeamUpdate = Map.from(t); if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t); } if (myScore > opponentScore) { myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1; oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1; } else if (myScore < opponentScore) { myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1; oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1; } else { myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1; oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1; } await supabase.from('teams').update({ 'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws'] }).eq('id', myTeamDbId!); await supabase.from('teams').update({ 'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws'] }).eq('id', oppTeamDbId!); gameWasAlreadyFinished = true; } // 3. Atualizar as Estatísticas dos Jogadores 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); batchStats.add({ 'game_id': gameId, 'member_id': memberDbId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!, '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'], 'ftm': stats['ftm'], 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'], }); } }); 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 e Resultados guardados 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(); } }