// // Match.swift // Padel Tournament // // Created by razmig on 10/03/2024. // import Foundation import LeStorage @Observable final public class Match: BaseMatch, SideStorable { static func == (lhs: Match, rhs: Match) -> Bool { lhs.id == rhs.id && lhs.startDate == rhs.startDate } public static func setServerTitle(upperRound: Round, matchIndex: Int) -> String { if upperRound.index == 0 { return upperRound.roundTitle() } return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted() } public var byeState: Bool = false public init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) { super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed) } required init(from decoder: Decoder) throws { try super.init(from: decoder) } required public init() { super.init() } // MARK: - DidSet public override func didSetStartDate() { if hasStarted() { return } if self.roundValue()?.tournamentObject()?.hasStarted() == false { plannedStartDate = startDate } else if self.groupStageValue()?.tournamentObject()?.hasStarted() == false { plannedStartDate = startDate } } // MARK: - public func setMatchName(_ serverName: String?) { self.name = serverName } public func isFromLastRound() -> Bool { guard let roundObject, roundObject.parent == nil else { return false } guard let currentTournament = currentTournament() else { return false } if currentTournament.rounds().count - 1 == roundObject.index { return true } else { return false } } public var tournamentStore: TournamentStore? { if let id = self.store?.identifier { return TournamentLibrary.shared.store(tournamentId: id) } fatalError("missing store for \(String(describing: type(of: self)))") } public var courtIndexForSorting: Int { courtIndex ?? Int.max } // MARK: - Computed dependencies public var teamScores: [TeamScore] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.teamScores.filter { $0.match == self.id } } // MARK: - public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { store.deleteDependencies(type: TeamScore.self, shouldBeSynchronized: shouldBeSynchronized) { $0.match == self.id } // let teamScores = self.teamScores // for teamScore in teamScores { // teamScore.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) // } // self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized) } public func indexInRound(in matches: [Match]? = nil) -> Int { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func indexInRound(in", matches?.count, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if groupStage != nil { return index } else if let matches, let index = matches.firstIndex(where: { $0.id == id }) { return index } else if let roundObject, roundObject.isUpperBracket(), let index = roundObject.playedMatches().firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) } public func matchWarningSubject() -> String { [roundTitle(), matchTitle(.short)].compacted().joined(separator: " ") } public func matchWarningMessage() -> String { [roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].compacted().joined(separator: "\n") } public func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func matchTitle", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if roundObject?.groupStageLoserBracket == true { return "\(index)\(index.ordinalFormattedSuffix()) place" } if let groupStageObject { return groupStageObject.localizedMatchUpLabel(for: index) } switch displayStyle { case .wide, .title: return "Match \(indexInRound(in: matches) + 1)" case .short: return "n˚\(indexInRound(in: matches) + 1)" } } public func isSeedLocked(atTeamPosition teamPosition: TeamPosition) -> Bool { return previousMatch(teamPosition)?.disabled == true } public func unlockSeedPosition(atTeamPosition teamPosition: TeamPosition) { previousMatch(teamPosition)?.enableMatch() } @discardableResult public func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int { let matchIndex = index previousMatch(teamPosition)?.disableMatch() return matchIndex * 2 + teamPosition.rawValue } public func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool { guard let roundObject, roundObject.isUpperBracket() else { return false } guard let bracketPosition = team.bracketPosition else { return false } return index * 2 + teamPosition.rawValue == bracketPosition } public func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? { let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration)) return startDate?.addingTimeInterval(minutesToAdd * 60.0) } public func winner() -> TeamRegistration? { guard let winningTeamId else { return nil } return self.tournamentStore?.teamRegistrations.findById(winningTeamId) } public func loser() -> TeamRegistration? { guard let losingTeamId else { return nil } return self.tournamentStore?.teamRegistrations.findById(losingTeamId) } public func localizedStartDate() -> String { if let startDate { return startDate.formatted(date: .abbreviated, time: .shortened) } else { return "" } } public func scoreLabel() -> String { if hasWalkoutTeam() == true { return "WO" } let scoreOne = teamScore(.one)?.score?.components(separatedBy: ",") ?? [] let scoreTwo = teamScore(.two)?.score?.components(separatedBy: ",") ?? [] let tuples = zip(scoreOne, scoreTwo).map { ($0, $1) } let scores = tuples.map { $0 + "/" + $1 }.joined(separator: " ") return scores } public func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { startDate = targetStartDate ?? startDate confirmed = false endDate = nil followingMatch()?.cleanScheduleAndSave(nil) _loserMatch()?.cleanScheduleAndSave(nil) self.tournamentStore?.matches.addOrUpdate(instance: self) } public func resetMatch() { losingTeamId = nil winningTeamId = nil endDate = nil removeCourt() servingTeamId = nil groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() teams().forEach({ $0.resetRestingTime() }) } public func resetScores() { teamScores.forEach({ $0.score = nil }) self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores) } public func teamWillBeWalkOut(_ team: TeamRegistration) { resetMatch() let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) existingTeamScore.walkOut = 1 self.tournamentStore?.teamScores.addOrUpdate(instance: existingTeamScore) } public func luckyLosers() -> [TeamRegistration] { return roundObject?.previousRound()?.losers() ?? [] } public func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool { return teamScore(teamPosition)?.walkOut == 1 } public func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) { resetMatch() let matchIndex = index let position = matchIndex * 2 + teamPosition.rawValue let previousScores = teamScores.filter({ $0.luckyLoser == position }) self.tournamentStore?.teamScores.delete(contentOfs: previousScores) let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) teamScoreLuckyLoser.luckyLoser = position self.tournamentStore?.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) } public func disableMatch() { _toggleMatchDisableState(true) } public func enableMatch() { _toggleMatchDisableState(false) } private func _loserMatch() -> Match? { let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) } public func _toggleLoserMatchDisableState(_ state: Bool) { guard let loserMatch = _loserMatch() else { return } guard let next = _otherMatch() else { return } loserMatch.byeState = true if next.disabled { loserMatch.byeState = false } loserMatch._toggleMatchDisableState(state, forward: true) } fileprivate func _otherMatch() -> Match? { guard let round else { return nil } guard index > 0 else { return nil } let nextIndex = (index - 1) / 2 let topMatchIndex = (nextIndex * 2) + 1 let bottomMatchIndex = (nextIndex + 1) * 2 let isTopMatch = topMatchIndex + 1 == index let lookingForIndex = isTopMatch ? topMatchIndex : bottomMatchIndex return self.tournamentStore?.matches.first(where: { $0.round == round && $0.index == lookingForIndex }) } private func _forwardMatch(inRound round: Round) -> Match? { guard let roundObjectNextRound = round.nextRound() else { return nil } let nextIndex = (index - 1) / 2 return self.tournamentStore?.matches.first(where: { $0.round == roundObjectNextRound.id && $0.index == nextIndex }) } public func _toggleForwardMatchDisableState(_ state: Bool) { guard let roundObject else { return } guard roundObject.parent != nil else { return } guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } guard let next = _otherMatch() else { return } if next.disabled && byeState == false && next.byeState == false { if forwardMatch.disabled != state || forwardMatch.byeState { forwardMatch.byeState = false forwardMatch._toggleMatchDisableState(state, forward: true) } } else if byeState && next.byeState { print("don't disable forward match") if forwardMatch.byeState || forwardMatch.disabled { forwardMatch.byeState = false forwardMatch._toggleMatchDisableState(false, forward: true) } } else if forwardMatch.byeState == false { forwardMatch.byeState = true forwardMatch._toggleMatchDisableState(false, forward: true) } else if forwardMatch.disabled != state { forwardMatch._toggleMatchDisableState(state, forward: true) } // if next.disabled == false { // forwardMatch.byeState = state // } // // if next.disabled == state { // if next.byeState != byeState { // //forwardMatch.byeState = state // forwardMatch._toggleMatchDisableState(state) // } else { // forwardMatch._toggleByeState(state) // } // } else { // } // forwardMatch._toggleByeState(state) } public func isSeededBy(team: TeamRegistration) -> Bool { isSeededBy(team: team, inTeamPosition: .one) || isSeededBy(team: team, inTeamPosition: .two) } public func isSeeded() -> Bool { return isSeededAt(.one) || isSeededAt(.two) } public func isSeededAt(_ teamPosition: TeamPosition) -> Bool { if let team = team(teamPosition) { return isSeededBy(team: team) } return false } public func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } let tournamentStore = self.tournamentStore let currentState = disabled disabled = state if disabled != currentState { tournamentStore?.teamScores.delete(contentOfs: teamScores) } if state == true, state != currentState { let teams = teams() for team in teams { if isSeededBy(team: team) { team.bracketPosition = nil tournamentStore?.teamRegistrations.addOrUpdate(instance: team) } } } //byeState = false if state != currentState { roundObject?.invalidateCache() name = nil tournamentStore?.matches.addOrUpdate(instance: self) } if single == false { _toggleLoserMatchDisableState(state) if forward { _toggleForwardMatchDisableState(state) } else { topPreviousRoundMatch()?._toggleMatchDisableState(state) bottomPreviousRoundMatch()?._toggleMatchDisableState(state) } } } public func next() -> Match? { guard let tournamentStore = self.tournamentStore else { return nil } let matches: [Match] = tournamentStore.matches.filter { $0.round == round && $0.index > index && $0.disabled == false } return matches.sorted(by: \.index).first } public func followingMatch() -> Match? { guard let nextRoundId = roundObject?.nextRound()?.id else { return nil } return getFollowingMatch(fromNextRoundId: nextRoundId) } public func getFollowingMatch(fromNextRoundId nextRoundId: String) -> Match? { return self.tournamentStore?.matches.first(where: { $0.round == nextRoundId && $0.index == (index - 1) / 2 }) } public func getDuration() -> Int { if let tournament = currentTournament() { matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) } else { matchFormat.getEstimatedDuration() } } public func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? { if groupStage != nil { return groupStageObject?.groupStageTitle() } else if let roundObject { return roundObject.roundTitle() } else { return nil } } public func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String { [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ") } public func topPreviousRoundMatchIndex() -> Int { return index * 2 + 1 } public func bottomPreviousRoundMatchIndex() -> Int { return (index + 1) * 2 } public func topPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } let topPreviousRoundMatchIndex = topPreviousRoundMatchIndex() let roundObjectPreviousRoundId = roundObject.previousRound()?.id return self.tournamentStore?.matches.first(where: { match in match.round != nil && match.round == roundObjectPreviousRoundId && match.index == topPreviousRoundMatchIndex }) } public func bottomPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } let bottomPreviousRoundMatchIndex = bottomPreviousRoundMatchIndex() let roundObjectPreviousRoundId = roundObject.previousRound()?.id return self.tournamentStore?.matches.first(where: { match in match.round != nil && match.round == roundObjectPreviousRoundId && match.index == bottomPreviousRoundMatchIndex }) } public func previousMatch(_ teamPosition: TeamPosition) -> Match? { if teamPosition == .one { return topPreviousRoundMatch() } else { return bottomPreviousRoundMatch() } } public func loserMatches() -> [Match] { guard let roundObject else { return [] } return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 }) } public func loserMatch(_ teamPosition: TeamPosition) -> Match? { if teamPosition == .one { return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil) } else { return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil) } } public var computedOrder: Int { if let groupStageObject { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + RoundRule.matchIndexWithinRound(fromMatchIndex: index) } public func previousMatches() -> [Match] { guard let roundObject else { return [] } guard let tournamentStore = self.tournamentStore else { return [] } let roundObjectPreviousRoundId = roundObject.previousRound()?.id return tournamentStore.matches.filter { match in match.round == roundObjectPreviousRoundId && (match.index == topPreviousRoundMatchIndex() || match.index == bottomPreviousRoundMatchIndex()) }.sorted(by: \.index) } public var matchFormat: MatchFormat { get { format ?? .defaultFormatForMatchType(.groupStage) } set { format = newValue } } public func removeWalkOut() { teamScores.forEach { teamScore in teamScore.walkOut = nil teamScore.score = nil } tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores) endDate = nil winningTeamId = nil losingTeamId = nil groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() updateFollowingMatchTeamScore() } public func setWalkOut(_ teamPosition: TeamPosition) { let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition)) teamScoreWalkout.walkOut = 0 teamScoreWalkout.score = matchFormat.defaultWalkOutScore(true).compactMap({ String($0) }).joined(separator: ",") let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam)) teamScoreWinning.walkOut = nil teamScoreWinning.score = matchFormat.defaultWalkOutScore(false).compactMap({ String($0) }).joined(separator: ",") do { try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) } catch { Logger.error(error) } if endDate == nil { endDate = Date() } teams().forEach({ $0.resetRestingTime() }) winningTeamId = teamScoreWinning.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() updateFollowingMatchTeamScore() } public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { updateScore(fromMatchDescriptor: matchDescriptor) if endDate == nil { endDate = Date() } if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) } else if let startDate, let endDate, startDate >= endDate { self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) } confirmed = true } public func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { updateScore(fromMatchDescriptor: matchDescriptor) if endDate == nil { endDate = Date() } if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) } else if let startDate, let endDate, startDate >= endDate { self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) } let teamOne = team(matchDescriptor.winner) let teamTwo = team(matchDescriptor.winner.otherTeam) teamOne?.hasArrived() teamTwo?.hasArrived() teamOne?.resetRestingTime() teamTwo?.resetRestingTime() winningTeamId = teamOne?.id losingTeamId = teamTwo?.id confirmed = true groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() if let tournament = currentTournament(), let endDate, let startDate { if endDate.isEarlierThan(tournament.startDate) { tournament.startDate = startDate } do { try DataStore.shared.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } } updateFollowingMatchTeamScore() } public func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one)) teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two)) teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false { teamScoreTwo.score = (matchDescriptor.teamTwoScores.dropLast() + [matchDescriptor.teamTwoScores.last! + "-0"]).joined(separator: ",") } else if matchDescriptor.teamTwoScores.last?.contains("-") == true && matchDescriptor.teamOneScores.last?.contains("-") == false { teamScoreOne.score = (matchDescriptor.teamOneScores.dropLast() + [matchDescriptor.teamOneScores.last! + "-0"]).joined(separator: ",") } do { try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo]) } catch { Logger.error(error) } matchFormat = matchDescriptor.matchFormat } public func updateFollowingMatchTeamScore() { followingMatch()?.updateTeamScores() _loserMatch()?.updateTeamScores() } public func resetTeamScores(outsideOf newTeamScores: [TeamScore]) { let ids = newTeamScores.map { $0.id } let teamScores = teamScores.filter({ ids.contains($0.id) == false }) if teamScores.isEmpty == false { self.tournamentStore?.teamScores.delete(contentOfs: teamScores) followingMatch()?.resetTeamScores(outsideOf: []) _loserMatch()?.resetTeamScores(outsideOf: []) } } public func createTeamScores() -> [TeamScore] { let teamOne = team(.one) let teamTwo = team(.two) let teams = [teamOne, teamTwo].compactMap({ $0 }).map { TeamScore(match: id, team: $0) } return teams } public func getOrCreateTeamScores() -> [TeamScore] { let teamOne = team(.one) let teamTwo = team(.two) let teams = [teamOne, teamTwo].compactMap({ $0 }).map { teamScore(ofTeam: $0) ?? TeamScore(match: id, team: $0) } return teams } public func updateTeamScores() { let teams = getOrCreateTeamScores() self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teams) resetTeamScores(outsideOf: teams) if teams.isEmpty == false { updateFollowingMatchTeamScore() } } public func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) { if hasEnded() == false { startDate = fromStartDate switch fieldSetup { case .fullRandom: if let _courtIndex = allCourts().randomElement() { setCourt(_courtIndex) } case .random: let runningMatches: [Match] = DataStore.shared.runningMatches() if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() { setCourt(_courtIndex) } case .field(let _courtIndex): setCourt(_courtIndex) } } else { startDate = fromStartDate endDate = toEndDate } if let startDate, startDate.timeIntervalSinceNow <= 300 { confirmed = true } else { confirmed = false } } public func courtName() -> String? { guard let courtIndex else { return nil } if let courtName = currentTournament()?.courtName(atIndex: courtIndex) { return courtName } else { return Court.courtIndexedTitle(atIndex: courtIndex) } } public func courtName(for selectedIndex: Int) -> String { if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) { return courtName } else { return Court.courtIndexedTitle(atIndex: selectedIndex) } } public func courtCount() -> Int { return currentTournament()?.courtCount ?? 1 } public func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool { let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] return courtUsed.contains(courtIndex) == false } public func courtIsPreferred(_ courtIndex: Int) -> Bool { return false } public func allCourts() -> [Int] { let availableCourts = Array(0.. [Int] { let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted() } public func removeCourt() { courtIndex = nil } public func setCourt(_ courtIndex: Int) { self.courtIndex = courtIndex } public func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool { let teams = teamScores guard teams.count == 2 else { //print("teams.count != 2") return false } guard hasEnded() == false else { return false } guard hasStarted() == false else { return false } return teams.compactMap({ $0.team }).allSatisfy({ ((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false }) } public func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { return matches.filter({ $0.teamScores.compactMap { $0.teamRegistration }.contains(team.id) }).isEmpty == false } public var computedStartDateForSorting: Date { return startDate ?? .distantFuture } public var computedEndDateForSorting: Date { return endDate ?? .distantFuture } public func hasSpaceLeft() -> Bool { return teamScores.count < 2 } public func isReady() -> Bool { return teamScores.count >= 2 // teams().count == 2 } public func isEmpty() -> Bool { return teamScores.isEmpty // teams().isEmpty } public func hasEnded() -> Bool { return endDate != nil } public func isGroupStage() -> Bool { return groupStage != nil } public func isBracket() -> Bool { return round != nil } public func walkoutTeam() -> [TeamRegistration] { //walkout 0 means real walkout, walkout 1 means lucky loser situation return scores().filter({ $0.walkOut == 0 }).compactMap { $0.team } } public func hasWalkoutTeam() -> Bool { return walkoutTeam().isEmpty == false } public func currentTournament() -> Tournament? { return groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject() } public func tournamentId() -> String? { return groupStageObject?.tournament ?? roundObject?.tournament } public func scores() -> [TeamScore] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.teamScores.filter { $0.match == id } } public func teams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func teams()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if groupStage != nil { return [groupStageProjectedTeam(.one), groupStageProjectedTeam(.two)].compactMap { $0 } } guard let roundObject else { return [] } let previousRound = roundObject.previousRound() return [roundObject.roundProjectedTeam(.one, inMatch: self, previousRound: previousRound), roundObject.roundProjectedTeam(.two, inMatch: self, previousRound: previousRound)].compactMap { $0 } // return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } public func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? { guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil } var reverseValue = 1 if teamPosition == team(.two)?.groupStagePositionAtStep(step) { reverseValue = -1 } let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) var setDifference : Int = 0 let zip = zip(endedSetsOne, endedSetsTwo) if matchFormat.setsToWin == 1 { setDifference = endedSetsOne[0] - endedSetsTwo[0] } else { setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count } // si 3 sets et 3eme set super tie break, different des 2 premiers sets, alors super tie points ne sont pas des jeux et doivent etre compté comme un jeu if matchFormat.canSuperTie, endedSetsOne.count == 3 { let games = zip.map { ($0, $1) } let gameDifference = games.enumerated().map({ index, pair in if index < 2 { return pair.0 - pair.1 } else { return pair.0 < pair.1 ? -1 : 1 } }) .reduce(0,+) return (setDifference * reverseValue, gameDifference * reverseValue) } else { let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) return (setDifference * reverseValue, gameDifference * reverseValue) } } public func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let groupStageObject else { return nil } return groupStageObject.team(teamPosition: team, inMatchIndex: index) } public func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let roundObject else { return nil } let previousRound = roundObject.previousRound() return roundObject.roundProjectedTeam(team, inMatch: self, previousRound: previousRound) } public func teamWon(_ team: TeamRegistration?) -> Bool { guard let winningTeamId else { return false } return winningTeamId == team?.id } public func teamWon(atPosition teamPosition: TeamPosition) -> Bool { guard let winningTeamId else { return false } return winningTeamId == team(teamPosition)?.id } public func team(_ team: TeamPosition) -> TeamRegistration? { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func match get team", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if groupStage != nil { return groupStageProjectedTeam(team) } else { return roundProjectedTeam(team) } } public func teamNames(_ team: TeamRegistration?) -> [String]? { return team?.players().map { $0.playerLabel() } } public func teamWalkOut(_ team: TeamRegistration?) -> Bool { return teamScore(ofTeam: team)?.isWalkOut() == true } public func teamScore(_ team: TeamPosition) -> TeamScore? { return teamScore(ofTeam: self.team(team)) } public func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? { return scores().first(where: { $0.teamRegistration == team?.id }) } public func isRunning() -> Bool { // at least a match has started return confirmed && hasStarted() && hasEnded() == false } public func hasStarted() -> Bool { // meaning at least one match is over if let startDate { return startDate.timeIntervalSinceNow < 0 && confirmed } if hasEnded() { return true } return false //todo scores // if let score { // return score.hasEnded == false && score.sets.isEmpty == false // } else { // return false // } } public var roundObject: Round? { guard let round else { return nil } return self.tournamentStore?.rounds.findById(round) } public var groupStageObject: GroupStage? { guard let groupStage else { return nil } return self.tournamentStore?.groupStages.findById(groupStage) } public var isLoserBracket: Bool { if let roundObject { if roundObject.parent != nil || roundObject.groupStageLoserBracket { return true } } return false } public var matchType: MatchType { if isLoserBracket { return .loserBracket } else if isGroupStage() { return .groupStage } else { return .bracket } } public var restingTimeForSorting: TimeInterval { (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow } public func isValidSpot() -> Bool { previousMatches().allSatisfy({ $0.isSeeded() == false }) } public func expectedToBeRunning() -> Bool { guard let startDate else { return false } return confirmed == false && startDate.timeIntervalSinceNow < 0 } public func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String { guard let startDate else { return "" } guard hasEnded() == false, isRunning() == false else { return "" } let depthReadiness = depthReadiness() if depthReadiness == 0 { if canBePlayedInSpecifiedCourt { return "possible tout de suite" } else if let updatedField, availableCourts.contains(updatedField) { return "possible tout de suite \(courtName(for: updatedField))" } else if let first = availableCourts.first { return "possible tout de suite \(courtName(for: first))" } else if let estimatedStartDate { return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0) } return "était prévu à " + startDate.formattedAsHourMinute() } else if depthReadiness == 1 { return "possible prochaine rotation" } else { return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())" } } public func runningDuration() -> String { guard let startDate else { return "" } return " depuis " + startDate.timeElapsedString() } public func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool { guard let courtIndex else { return false } if expectedToBeRunning() { return courtIsAvailable(courtIndex, in: runningMatches) } else { return true } } public typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date) public func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] { guard let tournament = currentTournament() else { return [] } let startDate = Date().withoutSeconds() if runningMatches.isEmpty { return availableCourts.map { ($0, startDate) } } let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil } guard let courtIndex = match.courtIndex else { return nil } if endDate <= startDate { return (courtIndex, startDate.addingTimeInterval(600)) } else { return (courtIndex, endDate) } }) let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in a.1 < b.1 } return dates } public func estimatedStartDate(availableCourts: [Int], runningMatches: [Match]) -> CourtIndexAndDate? { guard isReady() else { return nil } guard let tournament = currentTournament() else { return nil } let availableCourts = nextCourtsAvailable(availableCourts: availableCourts, runningMatches: runningMatches) return availableCourts.first(where: { (courtIndex, startDate) in let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60) if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false { return true } return false }) } public func depthReadiness() -> Int { // Base case: If this match is ready, the depth is 0 if isReady() { return 0 } // Recursive case: If not ready, check the maximum depth of readiness among previous matches // If previousMatches() is empty, return a default depth of -1 let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1 return previousDepth + 1 } public func ancestors() -> [Match] { previousMatches() + loserMatches() } public func matchSpots() -> [MatchSpot] { [MatchSpot(match: self, teamPosition: .one), MatchSpot(match: self, teamPosition: .two)] } func insertOnServer() { self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self) for teamScore in self.teamScores { teamScore.insertOnServer() } } } public enum MatchDateSetup: Hashable, Identifiable { case inMinutes(Int) case now case customDate case previousRotation case nextRotation public var id: Int { hashValue } } public enum MatchFieldSetup: Hashable, Identifiable { case random case fullRandom // case firstAvailable case field(Int) public var courtIndex: Int? { switch self { case .random: return nil case .fullRandom: return nil case .field(let int): return int } } public var id: Int { hashValue } }