// // Round.swift // Padel Tournament // // Created by razmig on 10/03/2024. // import Foundation import LeStorage import SwiftUI @Observable final class Round: ModelObject, Storable { static func resourceName() -> String { "rounds" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = [] var id: String = Store.randomId() var tournament: String var index: Int var parent: String? private(set) var format: MatchFormat? var startDate: Date? internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil) { self.tournament = tournament self.index = index self.parent = parent self.format = matchFormat self.startDate = startDate } // MARK: - Computed dependencies var tournamentStore: TournamentStore { return TournamentStore.instance(tournamentId: self.tournament) } func tournamentObject() -> Tournament? { return Store.main.findById(tournament) } func _matches() -> [Match] { return self.tournamentStore.matches.filter { $0.round == self.id }.sorted(by: \.index) // return Store.main.filter { $0.round == self.id } } func getDisabledMatches() -> [Match] { return self.tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true } // return Store.main.filter { $0.round == self.id && $0.disabled == true } } // MARK: - var matchFormat: MatchFormat { get { format ?? .defaultFormatForMatchType(.bracket) } set { format = newValue } } func hasStarted() -> Bool { return playedMatches().anySatisfy({ $0.hasStarted() }) } func hasEnded() -> Bool { if parent == nil { return playedMatches().anySatisfy({ $0.hasEnded() == false }) == false } else { return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false } } func upperMatches(ofMatch match: Match) -> [Match] { if parent != nil, previousRound() == nil, let parentRound { let matchIndex = match.index let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) return [parentRound.getMatch(atMatchIndexInRound: indexInRound * 2), parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 }) } return [] } func previousMatches(ofMatch match: Match) -> [Match] { guard let previousRound = previousRound() else { return [] } return self.tournamentStore.matches.filter { $0.round == previousRound.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) } // return Store.main.filter { // ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) && $0.round == previousRound.id // } } func precedentMatches(ofMatch match: Match) -> [Match] { let upper = upperMatches(ofMatch: match) if upper.isEmpty == false { return upper } let previous : [Match] = previousMatches(ofMatch: match) if previous.isEmpty == false && previous.allSatisfy({ $0.disabled }), let previousRound = previousRound() { return previous.flatMap({ previousRound.precedentMatches(ofMatch: $0) }) } else { return previous } } func team(_ team: TeamPosition, inMatch match: Match, previousRound: Round?) -> TeamRegistration? { return roundProjectedTeam(team, inMatch: match, previousRound: previousRound) } func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return self.tournamentStore.teamRegistrations.first(where: { $0.tournament == tournament && $0.bracketPosition != nil && ($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! % 2) == team.rawValue }) } func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] { return self.tournamentStore.teamRegistrations.filter { $0.tournament == tournament && $0.bracketPosition != nil && ($0.bracketPosition! / 2) == matchIndex } // return Store.main.filter(isIncluded: { // $0.tournament == tournament // && $0.bracketPosition != nil // && ($0.bracketPosition! / 2) == matchIndex // }) } func seeds() -> [TeamRegistration] { let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index) return self.tournamentStore.teamRegistrations.filter { $0.tournament == tournament && $0.bracketPosition != nil && ($0.bracketPosition! / 2) >= initialMatchIndex && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches } } func losers() -> [TeamRegistration] { let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } } func teams() -> [TeamRegistration] { return playedMatches().flatMap({ $0.teams() }) } func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match, previousRound: Round?) -> TeamRegistration? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func roundProjectedTeam", team.rawValue, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) { return seed } switch team { case .one: if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 }) { return luckyLoser.team } else if let previousMatch = topPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { if let teamId = previousMatch.winningTeamId { return self.tournamentStore.teamRegistrations.findById(teamId) } else if previousMatch.disabled { return previousMatch.teams().first } } else if let parent = upperBracketTopMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { return Store.main.findById(parent) } case .two: if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { return luckyLoser.team } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { if let teamId = previousMatch.winningTeamId { return self.tournamentStore.teamRegistrations.findById(teamId) } else if previousMatch.disabled { return previousMatch.teams().first } } else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { return Store.main.findById(parent) } } return nil } func upperBracketTopMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { return upperBracketTopMatch } return nil } func upperBracketBottomMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func upperBracketBottomMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { return upperBracketBottomMatch } return nil } func topPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func topPreviousRoundMatch", match.id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif guard let previousRound else { return nil } let topPreviousRoundMatchIndex = match.topPreviousRoundMatchIndex() return self.tournamentStore.matches.first(where: { $0.round == previousRound.id && $0.index == topPreviousRoundMatchIndex }) } func bottomPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func bottomPreviousRoundMatch", match.id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif guard let previousRound else { return nil } let bottomPreviousRoundMatchIndex = match.bottomPreviousRoundMatchIndex() return self.tournamentStore.matches.first(where: { $0.round == previousRound.id && $0.index == bottomPreviousRoundMatchIndex }) } func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? { self.tournamentStore.matches.first(where: { let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) return $0.round == id && index == matchIndexInRound }) } func enabledMatches() -> [Match] { return self.tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false }.sorted(by: \.index) } func displayableMatches() -> [Match] { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func displayableMatches of round: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if index == 0 && isUpperBracket() { var matches : [Match?] = [playedMatches().first] matches.append(loserRounds().first?.playedMatches().first) return matches.compactMap({ $0 }) } else { return playedMatches() } } func playedMatches() -> [Match] { if parent == nil { return enabledMatches() } else { return _matches() } } func previousRound() -> Round? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func previousRound of: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return self.tournamentStore.rounds.first(where: { $0.parent == parent && $0.index == index + 1 }) } func nextRound() -> Round? { return self.tournamentStore.rounds.first(where: { $0.parent == parent && $0.index == index - 1 }) } func loserRounds(forRoundIndex roundIndex: Int) -> [Round] { return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount) } func loserRounds(forRoundIndex roundIndex: Int, loserRoundsAndChildren: [Round]) -> [Round] { return loserRoundsAndChildren.filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount) } func isDisabled() -> Bool { return _matches().allSatisfy({ $0.disabled }) } func isRankDisabled() -> Bool { return _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty }) } func resetFromRoundAllMatchesStartDate() { _matches().forEach({ $0.startDate = nil }) loserRoundsAndChildren().forEach { round in round.resetFromRoundAllMatchesStartDate() } nextRound()?.resetFromRoundAllMatchesStartDate() } func resetFromRoundAllMatchesStartDate(from match: Match) { let matches = _matches() if let index = matches.firstIndex(where: { $0.id == match.id }) { matches[index...].forEach { match in match.startDate = nil } } loserRoundsAndChildren().forEach { round in round.resetFromRoundAllMatchesStartDate() } nextRound()?.resetFromRoundAllMatchesStartDate() } func getActiveLoserRound() -> Round? { let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed() return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first } func enableRound() { _toggleRound(disable: false) } func disableRound() { _toggleRound(disable: true) } private func _toggleRound(disable: Bool) { let _matches = _matches() _matches.forEach { match in match.disabled = disable match.resetMatch() //we need to keep teamscores to handle disable ranking match round stuff // do { // try DataStore.shared.teamScores.delete(contentOfs: match.teamScores) // } catch { // Logger.error(error) // } } do { try self.tournamentStore.matches.addOrUpdate(contentOfs: _matches) } catch { Logger.error(error) } } var cumulativeMatchCount: Int { var totalMatches = playedMatches().count if let parentRound { totalMatches += parentRound.cumulativeMatchCount } return totalMatches } func initialRound() -> Round? { if let parentRound { return parentRound.initialRound() } else { return self } } func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? { return enabledMatches().last?.estimatedEndDate(additionalEstimationDuration) } func getLoserRoundStartDate() -> Date? { return loserRoundsAndChildren().first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate } func estimatedLoserRoundEndDate(_ additionalEstimationDuration: Int) -> Date? { let lastMatch = loserRoundsAndChildren().last(where: { $0.isDisabled() == false })?.enabledMatches().last return lastMatch?.estimatedEndDate(additionalEstimationDuration) } func disabledMatches() -> [Match] { return _matches().filter({ $0.disabled }) } var theoryCumulativeMatchCount: Int { var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index) if let parentRound { totalMatches += parentRound.theoryCumulativeMatchCount } return totalMatches } func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func correspondingLoserRoundTitle()", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) let seedsAfterThisRound: [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { $0.bracketPosition != nil && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex } // let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: { // $0.tournament == tournament // && $0.bracketPosition != nil // && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex // }) let playedMatches = playedMatches() let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) return seedInterval.localizedLabel(displayStyle) } func hasNextRound() -> Bool { return nextRound()?.isRankDisabled() == false } func seedInterval(expanded: Bool = false) -> SeedInterval? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func seedInterval(expanded: Bool = false)", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if parent == nil { if index == 0 { return SeedInterval(first: 1, last: 2) } let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { $0.bracketPosition != nil && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex } let playedMatches = playedMatches() let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) return seedInterval } if let previousRound = previousRound() { if previousRound.enabledMatches().isEmpty == false && expanded == false { return previousRound.seedInterval()?.chunks()?.first } else { return previousRound.previousRound()?.seedInterval() } } else if let parentRound { if parentRound.parent == nil && expanded == false { return parentRound.seedInterval() } return parentRound.seedInterval()?.chunks()?.last } return nil } func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { if parent != nil { return seedInterval()?.localizedLabel(displayStyle) ?? "Round pas trouvé" } return RoundRule.roundName(fromRoundIndex: index, displayStyle: displayStyle) } func updateTournamentState() { if let tournamentObject = tournamentObject(), index == 0, isUpperBracket(), hasEnded() { tournamentObject.endDate = Date() do { try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) } catch { Logger.error(error) } } } func roundStatus() -> String { let hasEnded = hasEnded() if hasStarted() && hasEnded == false { return "en cours" } else if hasEnded { return "terminée" } else { return "à démarrer" } } func loserRounds() -> [Round] { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func loserRounds: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return self.tournamentStore.rounds.filter( { $0.parent == id }).sorted(by: \.index).reversed() } func loserRoundsAndChildren() -> [Round] { let loserRounds = loserRounds() return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() }) } func isUpperBracket() -> Bool { return parent == nil } func isLoserBracket() -> Bool { return parent != nil } func buildLoserBracket() { guard loserRounds().isEmpty else { return } let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) guard currentRoundMatchCount > 1 else { return } let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat let rounds = (0.. Bool { lhs.id == rhs.id } func selectionLabel(index: Int) -> String { if let parentRound { return "Tour #\(parentRound.loserRounds().count - index)" } else { return roundTitle(.short) } } func badgeValue() -> Int? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func badgeValue round of: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if let parentRound { return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count } else { return playedMatches().filter({ $0.isRunning() }).count } } func badgeValueColor() -> Color? { return nil } func badgeImage() -> Badge? { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func badgeImage of round: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return hasEnded() ? .checkmark : nil } }