using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.EntityFrameworkCore; using TournamentOrganizer.Models; namespace TournamentOrganizer.ViewModels; public partial class TournamentsViewModel : ViewModelBase { private readonly TournamentContext _context; [ObservableProperty] private ObservableCollection _availableEvents = []; [ObservableProperty] private EventOption? _selectedEvent; [ObservableProperty] private ObservableCollection _availableTournaments = []; [ObservableProperty] private TournamentOption? _selectedTournament; [ObservableProperty] private DateTime _tournamentStart; [ObservableProperty] private DateTime _tournamentEnd; [ObservableProperty] private RuleSetOption? _selectedS1RuleSet; [ObservableProperty] private int? _s1Groups; [ObservableProperty] private int? _s1GroupAdvances; [ObservableProperty] private RuleSetOption? _selectedS2RuleSet; [ObservableProperty] private ObservableCollection _availableTeams = []; [ObservableProperty] private ObservableCollection _participatingTeams = []; [ObservableProperty] private TeamOption? _selectedTeamToAdd; [ObservableProperty] private ParticipatingTeam? _selectedParticipatingTeam; [ObservableProperty] private ObservableCollection _matches = []; [ObservableProperty] private MatchDisplay? _selectedMatch; [ObservableProperty] private string _eventFilterSearch = string.Empty; [ObservableProperty] private string _tournamentFilterSearch = string.Empty; [ObservableProperty] private bool _isEventDropdownOpen; [ObservableProperty] private bool _isTournamentDropdownOpen; [ObservableProperty] private bool _isEditing; [ObservableProperty] private string _statusMessage = string.Empty; private readonly ObservableCollection _filteredEvents = []; public ObservableCollection FilteredEvents => _filteredEvents; private readonly ObservableCollection _filteredTournaments = []; public ObservableCollection FilteredTournaments => _filteredTournaments; public ObservableCollection S1RuleSetOptions { get; } = []; public ObservableCollection S2RuleSetOptions { get; } = [new RuleSetOption(null, "(None)")]; public bool IsTournamentFilterEnabled => SelectedEvent != null; public TournamentsViewModel() { _context = new TournamentContext(); foreach (var rs in Enum.GetValues()) { S1RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName())); S2RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName())); } } partial void OnEventFilterSearchChanged(string value) => UpdateFilteredEvents(); partial void OnSelectedEventChanged(EventOption? value) { OnPropertyChanged(nameof(IsTournamentFilterEnabled)); UpdateAvailableTournaments(); UpdateFilteredTournaments(); SelectedTournament = null; Matches.Clear(); ParticipatingTeams.Clear(); IsEditing = false; } partial void OnSelectedTournamentChanged(TournamentOption? value) { if (value == null) { IsEditing = false; Matches.Clear(); ParticipatingTeams.Clear(); return; } TournamentStart = value.Tournament.Start; TournamentEnd = value.Tournament.End; SelectedS1RuleSet = S1RuleSetOptions.FirstOrDefault(o => o.Value == value.Tournament.S1RuleSet); S1Groups = value.Tournament.S1Groups; S1GroupAdvances = value.Tournament.S1GroupAdvances; SelectedS2RuleSet = S2RuleSetOptions.FirstOrDefault(o => o.Value == value.Tournament.S2RuleSet); IsEditing = true; LoadTournamentDetails(); _ = LoadMatches(); StatusMessage = $"Editing tournament: {value.Tournament.Game.Name}"; } partial void OnSelectedS1RuleSetChanged(RuleSetOption? value) { _ = RegenerateMatches(); } partial void OnSelectedS2RuleSetChanged(RuleSetOption? value) { if (value?.Value == null) { S1Groups = null; S1GroupAdvances = null; } _ = RegenerateMatches(); } partial void OnTournamentFilterSearchChanged(string value) => UpdateFilteredTournaments(); private void UpdateFilteredEvents() { _filteredEvents.Clear(); foreach (var e in AvailableEvents.Where(e => string.IsNullOrWhiteSpace(EventFilterSearch) || e.DisplayName.ToLower().Contains(EventFilterSearch.ToLower()))) { _filteredEvents.Add(e); } } private void UpdateFilteredTournaments() { _filteredTournaments.Clear(); foreach (var t in AvailableTournaments.Where(t => string.IsNullOrWhiteSpace(TournamentFilterSearch) || t.DisplayName.ToLower().Contains(TournamentFilterSearch.ToLower()))) { _filteredTournaments.Add(t); } } public void SelectEventFilter(EventOption option) { SelectedEvent = option; IsEventDropdownOpen = false; EventFilterSearch = string.Empty; } public void ClearEventFilter() { SelectedEvent = null; EventFilterSearch = string.Empty; } public void SelectTournamentFilter(TournamentOption option) { SelectedTournament = option; IsTournamentDropdownOpen = false; TournamentFilterSearch = string.Empty; } public void ClearTournamentFilter() { SelectedTournament = null; TournamentFilterSearch = string.Empty; } public async Task LoadTournaments() { var events = await _context.Events .Include(e => e.Tournaments) .ThenInclude(t => t.Game) .ToListAsync(); AvailableEvents.Clear(); _filteredEvents.Clear(); foreach (var e in events) { var option = new EventOption(e); AvailableEvents.Add(option); _filteredEvents.Add(option); } UpdateAvailableTournaments(); } private void UpdateAvailableTournaments() { AvailableTournaments.Clear(); if (SelectedEvent == null) { return; } foreach (var t in SelectedEvent.Event.Tournaments) { AvailableTournaments.Add(new TournamentOption(t)); } } private async Task LoadTournamentDetails() { if (SelectedTournament == null) { return; } var tournamentTeams = await _context.TournamentTeams .Include(tt => tt.Team) .ThenInclude(t => t.Players) .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) .ToListAsync(); ParticipatingTeams.Clear(); foreach (var tt in tournamentTeams) { ParticipatingTeams.Add(new ParticipatingTeam(tt)); } SelectedParticipatingTeam = null; var allTeams = await _context.Teams .Include(t => t.Players) .ToListAsync(); var participatingIds = ParticipatingTeams.Select(pt => pt.TeamId).ToHashSet(); AvailableTeams.Clear(); foreach (var team in allTeams) { if (!participatingIds.Contains(team.Id)) { AvailableTeams.Add(new TeamOption(team)); } } SelectedTeamToAdd = null; } private async Task LoadMatches() { if (SelectedTournament == null) { return; } var matches = await _context.Matches .Where(m => m.TournamentId == SelectedTournament.Tournament.Id) .Include(m => m.Teams) .ThenInclude(tp => tp.Team) .Include(m => m.Rounds) .ThenInclude(r => r.Players) .ThenInclude(pp => pp.Player) .ToListAsync(); Matches.Clear(); foreach (var match in matches) { Matches.Add(new MatchDisplay(match)); } } [RelayCommand] private async Task SaveTournament() { if (SelectedTournament == null) { StatusMessage = "Select a tournament to edit"; return; } var tournament = await _context.Tournaments.FirstOrDefaultAsync(t => t.Id == SelectedTournament.Tournament.Id); if (tournament == null) { StatusMessage = "Tournament not found"; return; } tournament.Start = TournamentStart; tournament.End = TournamentEnd; tournament.S1RuleSet = SelectedS1RuleSet?.Value ?? tournament.S1RuleSet; tournament.S1Groups = S1Groups; tournament.S1GroupAdvances = S1GroupAdvances; tournament.S2RuleSet = SelectedS2RuleSet?.Value; await _context.SaveChangesAsync(); StatusMessage = "Tournament updated"; await LoadTournaments(); SelectedTournament = AvailableTournaments.FirstOrDefault(t => t.Tournament.Id == tournament.Id); } [RelayCommand] private async Task AddTeamToTournament() { if (SelectedTeamToAdd == null) { StatusMessage = "Select a team to add"; return; } if (SelectedTournament == null) { StatusMessage = "Select a tournament first"; return; } var tournamentTeams = await _context.TournamentTeams .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) .ToListAsync(); if (tournamentTeams.Any(tt => tt.TeamId == SelectedTeamToAdd.Team.Id)) { StatusMessage = "Team is already in this tournament"; return; } var seed = tournamentTeams.Count + 1; var tournamentTeam = new TournamentTeam { TournamentId = SelectedTournament.Tournament.Id, TeamId = SelectedTeamToAdd.Team.Id, Seed = seed, Tournament = SelectedTournament.Tournament, Team = SelectedTeamToAdd.Team }; _context.TournamentTeams.Add(tournamentTeam); await _context.SaveChangesAsync(); StatusMessage = $"Added '{SelectedTeamToAdd.Team.Name}' to tournament"; await LoadTournamentDetails(); await RegenerateMatches(); } [RelayCommand] private async Task RemoveTeamFromTournament() { if (SelectedParticipatingTeam == null) { StatusMessage = "Select a team to remove"; return; } if (SelectedTournament == null) { StatusMessage = "Select a tournament first"; return; } var tournamentTeam = await _context.TournamentTeams .FirstOrDefaultAsync(tt => tt.TournamentId == SelectedTournament.Tournament.Id && tt.TeamId == SelectedParticipatingTeam.TeamId); if (tournamentTeam != null) { _context.TournamentTeams.Remove(tournamentTeam); await _context.SaveChangesAsync(); StatusMessage = $"Removed '{SelectedParticipatingTeam.Name}' from tournament"; } await LoadTournamentDetails(); await RegenerateMatches(); } [RelayCommand] private async Task ReseedTeams() { if (SelectedTournament == null) { StatusMessage = "Select a tournament first"; return; } var tournamentTeams = await _context.TournamentTeams .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) .ToListAsync(); if (tournamentTeams.Count < 2) { StatusMessage = "Need at least 2 teams to reseed"; return; } var rng = new Random(); var shuffled = tournamentTeams.OrderBy(_ => rng.Next()).ToList(); for (int i = 0; i < shuffled.Count; i++) { shuffled[i].Seed = i + 1; } await _context.SaveChangesAsync(); StatusMessage = "Teams reseeded"; await LoadTournamentDetails(); await RegenerateMatches(); } private async Task RegenerateMatches() { if (SelectedTournament == null) { return; } var tournamentTeams = await _context.TournamentTeams .Include(tt => tt.Team) .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) .OrderBy(tt => tt.Seed) .ToListAsync(); var existingMatches = await _context.Matches .Where(m => m.TournamentId == SelectedTournament.Tournament.Id) .ToListAsync(); if (existingMatches.Count > 0) { _context.Matches.RemoveRange(existingMatches); await _context.SaveChangesAsync(); } if (tournamentTeams.Count < 2) { Matches.Clear(); return; } var tournament = await _context.Tournaments.FindAsync(SelectedTournament.Tournament.Id); if (tournament == null) { return; } var participants = tournamentTeams.Select(tt => new TeamParticipant { TeamId = tt.TeamId, Team = tt.Team, Seed = tt.Seed, Score = 0, MatchId = 0, Round = null! }).ToList(); var ruleSet = tournament.S2RuleSet ?? tournament.S1RuleSet; switch (ruleSet) { case RuleSet.SingleElimination: await GenerateSingleElimination(tournament, participants); break; case RuleSet.DoubleElimination: await GenerateDoubleElimination(tournament, participants); break; case RuleSet.RoundRobin: await GenerateRoundRobin(tournament, participants); break; case RuleSet.Swiss: await GenerateSwiss(tournament, participants); break; } await LoadMatches(); } private async Task GenerateSingleElimination(Tournament tournament, List teams) { int numTeams = teams.Count; int nextPowerOf2 = 1; while (nextPowerOf2 < numTeams) { nextPowerOf2 *= 2; } int totalRounds = (int)Math.Log2(nextPowerOf2); var allMatches = new List(); for (int round = 0; round < totalRounds; round++) { int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, round + 1); for (int i = 0; i < matchesInRound; i++) { var match = CreateMatch(tournament); if (round == 0) { int highSeed = i + 1; int lowSeed = nextPowerOf2 - i; if (highSeed <= numTeams) AddTeamToMatch(match, teams[highSeed - 1], highSeed); if (lowSeed <= numTeams) AddTeamToMatch(match, teams[lowSeed - 1], lowSeed); } allMatches.Add(match); } } _context.Matches.AddRange(allMatches); await _context.SaveChangesAsync(); } private async Task GenerateDoubleElimination(Tournament tournament, List teams) { int numTeams = teams.Count; int nextPowerOf2 = 1; while (nextPowerOf2 < numTeams) { nextPowerOf2 *= 2; } int wbRounds = (int)Math.Log2(nextPowerOf2); var allMatches = new List(); var wbMatchesByRound = new List>(); for (int r = 0; r < wbRounds; r++) { int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, r + 1); var roundMatches = new List(); for (int i = 0; i < matchesInRound; i++) { roundMatches.Add(CreateMatch(tournament)); } allMatches.AddRange(roundMatches); wbMatchesByRound.Add(roundMatches); } if (numTeams > 2) { var lbMatchesByRound = new List>(); for (int r = 0; r < wbRounds - 1; r++) { int matchesInRound = nextPowerOf2 / 4; var roundMatches = new List(); for (int i = 0; i < matchesInRound; i++) { roundMatches.Add(CreateMatch(tournament)); } allMatches.AddRange(roundMatches); lbMatchesByRound.Add(roundMatches); } var lbFinal = CreateMatch(tournament); allMatches.Add(lbFinal); var grandFinals = CreateMatch(tournament); allMatches.Add(grandFinals); } var firstRoundMatches = wbMatchesByRound[0]; for (int i = 0; i < nextPowerOf2 / 2; i++) { int highSeed = i + 1; int lowSeed = nextPowerOf2 - i; var match = firstRoundMatches[i]; if (highSeed <= numTeams) AddTeamToMatch(match, teams[highSeed - 1], highSeed); if (lowSeed <= numTeams) AddTeamToMatch(match, teams[lowSeed - 1], lowSeed); } _context.Matches.AddRange(allMatches); await _context.SaveChangesAsync(); } private static Match CreateMatch(Tournament tournament) { var match = new Match { TournamentId = tournament.Id, Tournament = tournament, Teams = [], Rounds = [] }; var r = new Round { MatchId = match.Id, Match = match, Players = [] }; match.Rounds.Add(r); return match; } private static void AddTeamToMatch(Match match, TeamParticipant team, int seed) { match.Teams.Add(new TeamParticipant { TeamId = team.TeamId, Seed = seed, Score = 0, Team = team.Team, MatchId = 0, Round = match }); } private async Task GenerateRoundRobin(Tournament tournament, List teams) { var matches = new List(); for (int i = 0; i < teams.Count; i++) { for (int j = i + 1; j < teams.Count; j++) { var match = new Match { TournamentId = tournament.Id, Tournament = tournament, Teams = [ new TeamParticipant { TeamId = teams[i].TeamId, Seed = teams[i].Seed, Score = 0, Team = teams[i].Team, MatchId = 0, Round = null! }, new TeamParticipant { TeamId = teams[j].TeamId, Seed = teams[j].Seed, Score = 0, Team = teams[j].Team, MatchId = 0, Round = null! } ], Rounds = [] }; var round = new Round { MatchId = match.Id, Match = match, Players = [] }; match.Rounds.Add(round); matches.Add(match); } } _context.Matches.AddRange(matches); await _context.SaveChangesAsync(); foreach (var match in matches) { foreach (var tp in match.Teams) { tp.Round = match; } } await _context.SaveChangesAsync(); } private async Task GenerateSwiss(Tournament tournament, List teams) { var matches = new List(); int numRounds = (int)Math.Ceiling(Math.Log2(teams.Count)); var shuffledTeams = teams.OrderBy(_ => Guid.NewGuid()).ToList(); for (int round = 0; round < numRounds; round++) { for (int i = 0; i < shuffledTeams.Count / 2; i++) { var team1 = shuffledTeams[i * 2]; var team2 = shuffledTeams[i * 2 + 1]; var match = new Match { TournamentId = tournament.Id, Tournament = tournament, Teams = [ new TeamParticipant { TeamId = team1.TeamId, Seed = team1.Seed, Score = 0, Team = team1.Team, MatchId = 0, Round = null! }, new TeamParticipant { TeamId = team2.TeamId, Seed = team2.Seed, Score = 0, Team = team2.Team, MatchId = 0, Round = null! } ], Rounds = [] }; var r = new Round { MatchId = match.Id, Match = match, Players = [] }; match.Rounds.Add(r); matches.Add(match); } } _context.Matches.AddRange(matches); await _context.SaveChangesAsync(); } } public class EventOption { public Event Event { get; set; } = null!; public string DisplayName { get; set; } = string.Empty; public EventOption() { } public EventOption(Event evt) { Event = evt; DisplayName = evt.Name; } public override string ToString() => DisplayName; } public class TournamentOption { public Tournament Tournament { get; set; } = null!; public string DisplayName { get; set; } = string.Empty; public TournamentOption() { } public TournamentOption(Tournament tournament) { Tournament = tournament; DisplayName = $"{tournament.Game.Name}"; } public override string ToString() => DisplayName; } public class TeamOption { public Team Team { get; set; } = null!; public string DisplayName { get; set; } = string.Empty; public TeamOption() { } public TeamOption(Team team) { Team = team; DisplayName = team.Name; } public override string ToString() => DisplayName; } public class ParticipatingTeam { public int TeamId { get; set; } public string Name { get; set; } = string.Empty; public int Seed { get; set; } public int Score { get; set; } public ParticipatingTeam() { } public ParticipatingTeam(TournamentTeam tt) { TeamId = tt.TeamId; Name = tt.Team.Name; Seed = tt.Seed; Score = 0; } } public class MatchDisplay { public int Id { get; set; } public string TeamsText { get; set; } = string.Empty; public RoundState State { get; set; } public MatchDisplay() { } public MatchDisplay(Match match) { Id = match.Id; if (match.Teams != null && match.Teams.Count >= 2) { TeamsText = $"{match.Teams[0].Team.Name} vs {match.Teams[1].Team.Name}"; } else if (match.Teams != null && match.Teams.Count == 1) { TeamsText = $"{match.Teams[0].Team.Name} vs TBD"; } State = match.Rounds?.LastOrDefault()?.State ?? RoundState.Waiting; } }