import 'dart:async'; import 'dart:math' as math; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ShotRecord { final double relativeX; final double relativeY; final bool isMake; final String playerId; final String playerName; final String? zone; final int? points; ShotRecord({ required this.relativeX, required this.relativeY, required this.isMake, required this.playerId, required this.playerName, this.zone, this.points, }); Map toJson() => { 'relativeX': relativeX, 'relativeY': relativeY, 'isMake': isMake, 'playerId': playerId, 'playerName': playerName, 'zone': zone, 'points': points, }; factory ShotRecord.fromJson(Map json) => ShotRecord( relativeX: json['relativeX'], relativeY: json['relativeY'], isMake: json['isMake'], playerId: json['playerId'], playerName: json['playerName'], zone: json['zone'], points: json['points'], ); } class PlacarController extends ChangeNotifier { final String gameId; final String myTeam; final String opponentTeam; PlacarController({ required this.gameId, required this.myTeam, required this.opponentTeam, }); bool isLoading = true; bool isSaving = false; 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 playerNames = {}; Map playerNumbers = {}; Map> playerStats = {}; bool showMyBench = false; bool showOppBench = false; bool isSelectingShotLocation = false; String? pendingAction; String? pendingPlayerId; List matchShots = []; List playByPlay = []; ValueNotifier durationNotifier = ValueNotifier(const Duration(minutes: 10)); Timer? timer; bool isRunning = false; bool isCalibrating = false; double hoopBaseX = 0.088; double arcRadius = 0.459; double cornerY = 0.440; Future loadPlayers() async { final supabase = Supabase.instance.client; try { await Future.delayed(const Duration(milliseconds: 1500)); myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear(); playerNames.clear(); playerStats.clear(); playerNumbers.clear(); matchShots.clear(); playByPlay.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; durationNotifier.value = 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; gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; if (gameResponse['play_by_play'] != null) { playByPlay = List.from(gameResponse['play_by_play']); } else { playByPlay = []; } 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]; _loadSavedPlayerStats(dbId, s); 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]; _loadSavedPlayerStats(dbId, s); opponentFouls += (s['fls'] as int? ?? 0); } } _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); final shotsResponse = await supabase.from('shot_locations').select().eq('game_id', gameId); for (var shotData in shotsResponse) { matchShots.add(ShotRecord( relativeX: double.parse(shotData['relative_x'].toString()), relativeY: double.parse(shotData['relative_y'].toString()), isMake: shotData['is_make'] == true, playerId: shotData['member_id'].toString(), playerName: shotData['player_name'].toString(), zone: shotData['zone']?.toString(), points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null, )); } await _loadLocalBackup(); isLoading = false; notifyListeners(); } catch (e) { debugPrint("Erro ao retomar jogo: $e"); isLoading = false; notifyListeners(); } } void _loadSavedPlayerStats(String dbId, Map s) { playerStats[dbId] = { "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, "p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0, "so": s['so'] ?? 0, "il": s['il'] ?? 0, "li": s['li'] ?? 0, "pa": s['pa'] ?? 0, "tres_s": s['tres_s'] ?? 0, "dr": s['dr'] ?? 0, "min": s['min'] ?? 0, }; } void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) { String id = dbId ?? "fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}"; playerNames[id] = name; playerNumbers[id] = number; playerStats[id] = { "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, "p2m": 0, "p2a": 0, "p3m": 0, "p3a": 0, "so": 0, "il": 0, "li": 0, "pa": 0, "tres_s": 0, "dr": 0, "min": 0 }; if (isMyTeam) { if (isCourt) myCourt.add(id); else myBench.add(id); } else { if (isCourt) oppCourt.add(id); else oppBench.add(id); } } 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); } } Future _saveLocalBackup() async { try { final prefs = await SharedPreferences.getInstance(); final backupData = { 'myScore': myScore, 'opponentScore': opponentScore, 'myFouls': myFouls, 'opponentFouls': opponentFouls, 'currentQuarter': currentQuarter, 'duration': durationNotifier.value.inSeconds, 'myTimeoutsUsed': myTimeoutsUsed, 'opponentTimeoutsUsed': opponentTimeoutsUsed, 'playerStats': playerStats, 'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench, 'matchShots': matchShots.map((s) => s.toJson()).toList(), 'playByPlay': playByPlay, }; await prefs.setString('backup_$gameId', jsonEncode(backupData)); } catch (e) { debugPrint("Erro no Auto-Save: $e"); } } Future _loadLocalBackup() async { try { final prefs = await SharedPreferences.getInstance(); final String? backupString = prefs.getString('backup_$gameId'); if (backupString != null) { final data = jsonDecode(backupString); myScore = data['myScore']; opponentScore = data['opponentScore']; myFouls = data['myFouls']; opponentFouls = data['opponentFouls']; currentQuarter = data['currentQuarter']; durationNotifier.value = Duration(seconds: data['duration']); myTimeoutsUsed = data['myTimeoutsUsed']; opponentTimeoutsUsed = data['opponentTimeoutsUsed']; myCourt = List.from(data['myCourt']); myBench = List.from(data['myBench']); oppCourt = List.from(data['oppCourt']); oppBench = List.from(data['oppBench']); Map decodedStats = data['playerStats']; playerStats = decodedStats.map((k, v) => MapEntry(k, Map.from(v))); List decodedShots = data['matchShots']; matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList(); playByPlay = List.from(data['playByPlay'] ?? []); } } catch (e) { debugPrint("Erro ao carregar Auto-Save: $e"); } } void toggleTimer(BuildContext context) { if (isRunning) { timer?.cancel(); _saveLocalBackup(); } else { timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (durationNotifier.value.inSeconds > 0) { durationNotifier.value -= const Duration(seconds: 1); } else { timer.cancel(); isRunning = false; if (currentQuarter < 4) { currentQuarter++; durationNotifier.value = const Duration(minutes: 10); myFouls = 0; opponentFouls = 0; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0; _saveLocalBackup(); } notifyListeners(); } }); } isRunning = !isRunning; notifyListeners(); } void useTimeout(bool isOpponent) { if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; } else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; } isRunning = false; timer?.cancel(); _saveLocalBackup(); notifyListeners(); } void handleActionDrag(BuildContext context, String action, String playerData) { String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[playerId]!; final name = playerNames[playerId]!; 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; pendingPlayerId = playerData; isSelectingShotLocation = true; } else { commitStat(action, playerData); } notifyListeners(); } void handleSubbing(BuildContext context, String action, String courtPlayerId, bool isOpponent) { if (action.startsWith("bench_my_") && !isOpponent) { String benchPlayerId = action.replaceAll("bench_my_", ""); if (playerStats[benchPlayerId]!["fls"]! >= 5) return; int courtIndex = myCourt.indexOf(courtPlayerId); int benchIndex = myBench.indexOf(benchPlayerId); myCourt[courtIndex] = benchPlayerId; myBench[benchIndex] = courtPlayerId; showMyBench = false; } if (action.startsWith("bench_opp_") && isOpponent) { String benchPlayerId = action.replaceAll("bench_opp_", ""); if (playerStats[benchPlayerId]!["fls"]! >= 5) return; int courtIndex = oppCourt.indexOf(courtPlayerId); int benchIndex = oppBench.indexOf(benchPlayerId); oppCourt[courtIndex] = benchPlayerId; oppBench[benchIndex] = courtPlayerId; showOppBench = false; } _saveLocalBackup(); notifyListeners(); } void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) { String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", ""); bool isMake = action.startsWith("add_"); String name = playerNames[playerId] ?? "Jogador"; matchShots.add(ShotRecord( relativeX: relativeX, relativeY: relativeY, isMake: isMake, playerId: playerId, playerName: name, zone: zone, points: points )); String finalAction = isMake ? "add_pts_$points" : "miss_$points"; commitStat(finalAction, targetPlayer); notifyListeners(); } void registerShotLocation(BuildContext context, Offset position, Size size) { if (pendingAction == null || pendingPlayerId == null) return; bool is3Pt = pendingAction!.contains("_3"); bool is2Pt = pendingAction!.contains("_2"); if (is3Pt || is2Pt) { bool isValid = _validateShotZone(position, size, is3Pt); if (!isValid) return; } bool isMake = pendingAction!.startsWith("add_pts_"); double relX = position.dx / size.width; double relY = position.dy / size.height; String pId = pendingPlayerId!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerId: pId, playerName: playerNames[pId]!)); commitStat(pendingAction!, pendingPlayerId!); isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; _saveLocalBackup(); notifyListeners(); } bool _validateShotZone(Offset position, Size size, bool is3Pt) { double relX = position.dx / size.width; double relY = position.dy / size.height; bool isLeftHalf = relX < 0.5; double hoopX = isLeftHalf ? hoopBaseX : (1.0 - hoopBaseX); double hoopY = 0.50; double aspectRatio = size.width / size.height; double distFromCenterY = (relY - hoopY).abs(); bool isInside2Pts; if (distFromCenterY > cornerY) { double distToBaseline = isLeftHalf ? relX : (1.0 - relX); isInside2Pts = distToBaseline <= hoopBaseX; } else { double dx = (relX - hoopX) * aspectRatio; double dy = (relY - hoopY); double distanceToHoop = math.sqrt((dx * dx) + (dy * dy)); isInside2Pts = distanceToHoop < arcRadius; } if (is3Pt) return !isInside2Pts; return isInside2Pts; } void cancelShotLocation() { isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; notifyListeners(); } void commitStat(String action, String playerData) { bool isOpponent = playerData.startsWith("player_opp_"); String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[playerId]!; final name = playerNames[playerId] ?? "Jogador"; String logText = ""; 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) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; stats["p2m"] = stats["p2m"]! + 1; stats["p2a"] = stats["p2a"]! + 1; } if (pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; stats["p3m"] = stats["p3m"]! + 1; stats["p3a"] = stats["p3a"]! + 1; } if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; } logText = "marcou $pts pontos 🏀"; } else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; logText = "falhou lance livre ❌"; } else if (action == "miss_2") { stats["fga"] = stats["fga"]! + 1; stats["p2a"] = stats["p2a"]! + 1; logText = "falhou lançamento de 2 ❌"; } else if (action == "miss_3") { stats["fga"] = stats["fga"]! + 1; stats["p3a"] = stats["p3a"]! + 1; logText = "falhou lançamento de 3 ❌"; } else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto ofensivo 🔄"; } else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto defensivo 🛡️"; } else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; logText = "fez uma assistência 🤝"; } else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; logText = "roubou a bola 🥷"; } else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; logText = "fez um desarme (bloco) ✋"; } else if (action == "add_foul") { stats["fls"] = stats["fls"]! + 1; if (isOpponent) opponentFouls++; else myFouls++; logText = "cometeu falta ⚠️"; } else if (action == "add_so") { stats["so"] = stats["so"]! + 1; logText = "sofreu uma falta 🤕"; } else if (action == "add_il") { stats["il"] = stats["il"]! + 1; logText = "intercetou um lançamento 🛑"; } else if (action == "add_li") { stats["li"] = stats["li"]! + 1; logText = "teve o lançamento intercetado 🚫"; } // Registo avançado de Bolas Perdidas (TOV) else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; logText = "fez um passe ruim 🤦"; } else if (action == "add_pa") { stats["pa"] = stats["pa"]! + 1; stats["tov"] = stats["tov"]! + 1; logText = "cometeu passos 🚶"; } else if (action == "add_3s") { stats["tres_s"] = stats["tres_s"]! + 1; stats["tov"] = stats["tov"]! + 1; logText = "violação de 3 segundos ⏱️"; } else if (action == "add_24s") { stats["tov"] = stats["tov"]! + 1; logText = "violação de 24 segundos ⏱️"; } else if (action == "add_dr") { stats["dr"] = stats["dr"]! + 1; stats["tov"] = stats["tov"]! + 1; logText = "fez drible duplo 🏀"; } if (logText.isNotEmpty) { String time = "${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}"; playByPlay.insert(0, "P$currentQuarter - $time: $name $logText"); } _saveLocalBackup(); } Future saveGameStats(BuildContext context) async { final supabase = Supabase.instance.client; isSaving = true; notifyListeners(); try { bool isGameFinishedNow = currentQuarter >= 4 && durationNotifier.value.inSeconds == 0; String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; String topPtsName = '---'; int maxPts = -1; String topAstName = '---'; int maxAst = -1; String topRbsName = '---'; int maxRbs = -1; String mvpName = '---'; double maxMvpScore = -999.0; playerStats.forEach((playerId, stats) { int pts = stats['pts'] ?? 0; int ast = stats['ast'] ?? 0; int rbs = stats['rbs'] ?? 0; int minJogados = (stats['min'] ?? 0) > 0 ? stats['min']! : 40; int tr = rbs; int br = stats['stl'] ?? 0; int bp = stats['tov'] ?? 0; int lFalhados = (stats['fga'] ?? 0) - (stats['fgm'] ?? 0); int llFalhados = (stats['fta'] ?? 0) - (stats['ftm'] ?? 0); double mvpScore = ((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) - ((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35)); mvpScore = mvpScore * (minJogados / 40.0); String pName = playerNames[playerId] ?? '---'; if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$pName ($pts)'; } if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$pName ($ast)'; } if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$pName ($rbs)'; } if (mvpScore > maxMvpScore) { maxMvpScore = mvpScore; mvpName = '$pName (${mvpScore.toStringAsFixed(1)})'; } }); await supabase.from('games').update({ 'my_score': myScore, 'opponent_score': opponentScore, 'remaining_seconds': durationNotifier.value.inSeconds, 'my_timeouts': myTimeoutsUsed, 'opp_timeouts': opponentTimeoutsUsed, 'current_quarter': currentQuarter, 'status': newStatus, 'top_pts_name': topPtsName, 'top_ast_name': topAstName, 'top_rbs_name': topRbsName, 'mvp_name': mvpName, 'play_by_play': playByPlay, }).eq('id', gameId); List> batchStats = []; playerStats.forEach((playerId, stats) { if (!playerId.startsWith("fake_")) { bool isMyTeamPlayer = myCourt.contains(playerId) || myBench.contains(playerId); batchStats.add({ 'game_id': gameId, 'member_id': playerId, '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'], 'p2m': stats['p2m'], 'p2a': stats['p2a'], 'p3m': stats['p3m'], 'p3a': stats['p3a'], 'so': stats['so'], 'il': stats['il'], 'li': stats['li'], 'pa': stats['pa'], 'tres_s': stats['tres_s'], 'dr': stats['dr'], 'min': stats['min'], }); } }); await supabase.from('player_stats').delete().eq('game_id', gameId); if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats); final prefs = await SharedPreferences.getInstance(); await prefs.remove('backup_$gameId'); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Guardado 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; notifyListeners(); } } @override void dispose() { timer?.cancel(); super.dispose(); } void registerFoul(String s, String foulType, String t) {} }