You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Extensions/Tournament+Extensions.swift

464 lines
17 KiB

//
// Tournament+Extensions.swift
// PadelClub
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
import SwiftUI
import PadelClubData
import LeStorage
extension Tournament {
func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
registrationDateLimit = deadline(for: .inscription)
if enableOnlineRegistration, isAnimation() == false {
accountIsRequired = true
licenseIsRequired = true
}
}
func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.dayDuration = lastTournamentWithSameBuild.dayDuration
self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2
self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in
player.teamRegistration = team.id
}
if isAnimation() {
if team.weight == 0 {
team.weight = unsortedTeams().count
}
}
return team
}
func addWildCardIfNeeded(_ count: Int, _ type: MatchType) {
let currentCount = selectedSortedTeams().filter({
if type == .bracket {
return $0.wildCardBracket
} else {
return $0.wildCardGroupStage
}
}).count
if currentCount < count {
let _diff = count - currentCount
addWildCard(_diff, type)
}
}
func addEmptyTeamRegistration(_ count: Int) {
guard let tournamentStore = self.tournamentStore else { return }
let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
func addWildCard(_ count: Int, _ type: MatchType) {
let wcs = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
if type == .bracket {
team.wildCardBracket = true
} else {
team.wildCardGroupStage = true
}
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
team.weight += 200_000
return team
}
do {
try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: wcs)
} catch {
Logger.error(error)
}
}
func teamsRanked() -> [TeamRegistration] {
let selected = selectedSortedTeams().filter({ $0.finalRanking != nil })
return selected.sorted(by: \.finalRanking!, order: .ascending)
}
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] {
let licenseYearValidity = self.licenseYearValidity()
return players.filter({ player in
if player.isImported() {
// Player is marked as imported: check if the license is valid
return !player.isValidLicenseNumber(year: licenseYearValidity)
} else {
// Player is not imported: validate license and handle `isImported` flag for non-imported players
let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true
let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false
// If global `isImported` is true, check license number as well
let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity)
return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag
}
})
}
func homonyms(in players: [PlayerRegistration]) -> [PlayerRegistration] {
players.filter({ $0.hasHomonym() })
}
func payIfNecessary() throws {
if self.payment != nil { return }
if let payment = Guard.main.paymentForNewTournament() {
self.payment = payment
DataStore.shared.tournaments.addOrUpdate(instance: self)
return
}
throw PaymentError.cantPayTournament
}
func cutLabelColor(index: Int?, teamCount: Int?) -> Color {
guard let index else { return Color.grayNotUniversal }
let _teamCount = teamCount ?? selectedSortedTeams().count
let groupStageCut = groupStageCut()
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
if index < bracketCut {
return Color.mint
} else if index - bracketCut < groupStageCut && _teamCount > 0 {
return Color.indigo
} else {
return Color.grayNotUniversal
}
}
func isPlayerAgeInadequate(player: PlayerHolder) -> Bool {
guard let computedAge = player.computedAge else { return false }
if federalTournamentAge.isAgeValid(age: computedAge) == false {
return true
} else {
return false
}
}
func isPlayerRankInadequate(player: PlayerHolder) -> Bool {
guard let rank = player.getRank() else { return false }
let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0)
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
return true
} else {
return false
}
}
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
if startDate.isInCurrentYear() == false {
return []
}
return players.filter { player in
return isPlayerRankInadequate(player: player)
}
}
func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
if startDate.isInCurrentYear() == false {
return []
}
return players.filter { player in
return isPlayerAgeInadequate(player: player)
}
}
func importTeams(_ teams: [FileImportManager.TeamHolder]) {
var teamsToImport = [TeamRegistration]()
let players = players().filter { $0.licenceId != nil }
teams.forEach { team in
if let previousTeam = team.previousTeam {
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
teamsToImport.append(previousTeam)
} else {
var registrationDate = team.registrationDate
if let previousPlayer = players.first(where: { player in
let ids = team.players.compactMap({ $0.licenceId })
return ids.contains(player.licenceId!)
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate {
registrationDate = previousTeamRegistrationDate
}
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name)
if isAnimation() {
if newTeam.weight == 0 {
newTeam.weight = team.index(in: teams) ?? 0
}
}
teamsToImport.append(newTeam)
}
}
if let tournamentStore = self.tournamentStore {
tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport)
let playersToImport = teams.flatMap { $0.players }
tournamentStore.playerRegistrations.addOrUpdate(contentOfs: playersToImport)
}
if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty {
setGroupStage(randomize: groupStageSortMode == .random)
}
}
func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
let homonyms = homonyms(in: players)
let ageInadequatePlayers = ageInadequatePlayers(in: players)
let isImported = players.anySatisfy({ $0.isImported() })
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported)
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams, includingWalkOuts: true)
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil })
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil })
return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count
}
func updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws {
refreshRanking = true
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let newDate else { return }
rankSourceDate = newDate
// Fetch current month data only once
var monthData = currentMonthData()
if monthData == nil {
async let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
async let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
let formatted = URL.importDateFormatter.string(from: newDate)
let newMonthData = MonthData(monthKey: formatted)
newMonthData.maleUnrankedValue = await lastRankMan
newMonthData.femaleUnrankedValue = await lastRankWoman
do {
try DataStore.shared.monthData.addOrUpdate(instance: newMonthData)
} catch {
Logger.error(error)
}
monthData = newMonthData
}
let lastRankMan = monthData?.maleUnrankedValue
let lastRankWoman = monthData?.femaleUnrankedValue
var chunkedParsers: [CSVParser] = []
if let providedSources {
chunkedParsers = providedSources
} else {
// Fetch only the required files
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
guard !dataURLs.isEmpty else { return } // Early return if no files found
let sources = dataURLs.map { CSVParser(url: $0) }
chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
}
let players = unsortedPlayers()
try await players.concurrentForEach { player in
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
try await player.updateRank(from: chunkedParsers, lastRank: lastRank)
player.setComputedRank(in: self)
}
if providedSources == nil {
try chunkedParsers.forEach { chunk in
try FileManager.default.removeItem(at: chunk.url)
}
}
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
let unsortedTeams = unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
}
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
refreshRanking = false
}
}
extension Tournament {
static func newEmptyInstance() -> Tournament {
let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource
var _mostRecentDateAvailable: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
let rankSourceDate = _mostRecentDateAvailable
return Tournament(rankSourceDate: rankSourceDate)
}
}
extension Tournament: FederalTournamentHolder {
func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() {
if let name {
return name.trunc(length: DeviceHelper.charLength())
} else if build.age == .unlisted, build.category == .unlisted {
return build.level.localizedLevelLabel(.title)
} else {
return build.level.localizedLevelLabel(displayStyle)
}
}
return build.level.localizedLevelLabel(displayStyle)
}
var codeClub: String? {
club()?.code
}
var holderId: String { id }
func clubLabel() -> String {
locationLabel()
}
func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() {
if displayAgeAndCategory(forBuild: build) == false {
return [build.category.localizedCategoryLabel(ageCategory: build.age), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ")
} else if name != nil {
return build.level.localizedLevelLabel(.title)
} else {
return ""
}
} else {
return subtitle()
}
}
var tournaments: [any TournamentBuildHolder] {
[
self
]
}
var dayPeriod: DayPeriod {
let day = startDate.get(.weekday)
switch day {
case 2...6:
return .week
default:
return .weekend
}
}
func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool {
if isAnimation() {
if let name, name.count < DeviceHelper.maxCharacter() {
return true
} else if build.age == .unlisted, build.category == .unlisted {
return true
} else {
return DeviceHelper.isBigScreen()
}
}
return true
}
}
extension Tournament: TournamentBuildHolder {
public func buildHolderTitle(_ displayStyle: DisplayStyle) -> String {
tournamentTitle(.short)
}
public var category: TournamentCategory {
tournamentCategory
}
public var level: TournamentLevel {
tournamentLevel
}
public var age: FederalTournamentAge {
federalTournamentAge
}
}
// MARK: - UI extensions
extension Tournament {
public var shouldShowPaymentInfo: Bool {
if self.payment != nil {
return false
}
switch self.state() {
case .initial, .build, .running:
return true
default:
return false
}
}
}
//extension Tournament {
// func deadline(for type: TournamentDeadlineType) -> Date? {
// guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
//
// let daysOffset = type.daysOffset(level: tournamentLevel)
// if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
// let startOfDay = Calendar.current.startOfDay(for: date)
// return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
// }
// return nil
// }
//}