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/Views/Tournament/Screen/TableStructureView.swift

888 lines
37 KiB

//
// TableStructureView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 04/10/2023.
//
import SwiftUI
import LeStorage
import PadelClubData
struct TableStructureView: View {
var tournament: Tournament
@EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss
@State private var presentRefreshStructureWarning: Bool = false
@State private var teamCount: Int = 0
@State private var groupStageCount: Int = 0
@State private var teamsPerGroupStage: Int = 0
@State private var qualifiedPerGroupStage: Int = 0
@State private var groupStageAdditionalQualified: Int = 0
@State private var updatedElements: Set<StructureElement> = Set()
@State private var structurePreset: PadelTournamentStructurePreset = .manual
@State private var buildWildcards: Bool = true
@FocusState private var stepperFieldIsFocused: Bool
@State private var confirmReset: Bool = false
@State private var selectedTournament: Tournament?
@State private var initialSeedCount: Int = 0
@State private var initialSeedRound: Int = 0
@State private var showSeedRepartition: Bool = false
@State private var seedRepartition: [Int] = []
func displayWarning() -> Bool {
let unsortedTeamsCount = tournament.unsortedTeamsCount()
return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount)
}
var qualifiedFromGroupStage: Int {
groupStageCount * qualifiedPerGroupStage
}
var teamsFromGroupStages: Int {
groupStageCount * teamsPerGroupStage
}
var maxMoreQualified: Int {
if teamsPerGroupStage - qualifiedPerGroupStage > 1 {
return groupStageCount
} else if teamsPerGroupStage - qualifiedPerGroupStage == 1 {
return groupStageCount - 1
} else {
return 0
}
}
var moreQualifiedLabel: String {
if groupStageAdditionalQualified == 0 { return "Aucun" }
return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " " + (qualifiedPerGroupStage + 1).ordinalFormatted()
}
var maxGroupStages: Int {
teamCount / max(1, teamsPerGroupStage)
}
var tsPure: Int {
max(teamCount - groupStageCount * teamsPerGroupStage, 0)
}
var tf: Int {
max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
}
init(tournament: Tournament) {
self.tournament = tournament
_teamCount = .init(wrappedValue: tournament.teamCount)
_groupStageCount = .init(wrappedValue: tournament.groupStageCount)
_teamsPerGroupStage = .init(wrappedValue: tournament.teamsPerGroupStage)
_qualifiedPerGroupStage = .init(wrappedValue: tournament.qualifiedPerGroupStage)
_groupStageAdditionalQualified = .init(wrappedValue: tournament.groupStageAdditionalQualified)
_initialSeedCount = .init(wrappedValue: tournament.initialSeedCount)
_initialSeedRound = .init(wrappedValue: tournament.initialSeedRound)
}
var body: some View {
List {
if displayWarning() {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.")
.foregroundStyle(.logoRed)
}
if tournament.state() != .build {
Section {
Picker(selection: $structurePreset) {
ForEach(PadelTournamentStructurePreset.allCases) { preset in
Text(preset.localizedStructurePresetTitle()).tag(preset)
}
} label: {
Text("Préréglage")
}
.disabled(selectedTournament != nil)
} footer: {
Text(structurePreset.localizedDescriptionStructurePresetTitle())
}
.onChange(of: structurePreset) {
_updatePreset()
}
Section {
NavigationLink {
TournamentSelectorView(selectedTournament: $selectedTournament)
.environment(tournament)
} label: {
if let selectedTournament {
TournamentCellView(tournament: selectedTournament, displayContext: .selection)
} else {
Text("À partir d'un tournoi existant")
}
}
}
.onChange(of: selectedTournament) {
_updatePreset()
}
}
Section {
LabeledContent {
StepperView(count: $teamCount, minimum: 4, maximum: 128) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Nombre d'équipes")
let minimumNumberOfTeams = tournament.minimumNumberOfTeams()
if minimumNumberOfTeams > 0 {
Text("Minimum pour homologation : \(minimumNumberOfTeams)")
.foregroundStyle(.secondary)
}
}
LabeledContent {
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Nombre de poules")
}
} footer: {
if groupStageCount > 0 {
Text("Vous pourrez modifier la taille de vos poules de manière spécifique dans l'écran des poules.")
}
}
if groupStageCount > 0 {
if (teamCount / groupStageCount) > 1 {
Section {
LabeledContent {
StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount)) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Équipes par poule")
}
if structurePreset != .doubleGroupStage {
LabeledContent {
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule")
}
if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
LabeledContent {
StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires")
Text(moreQualifiedLabel)
}
.onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified == groupStageCount {
qualifiedPerGroupStage += 1
groupStageAdditionalQualified -= groupStageCount
}
}
}
}
if groupStageCount > 0 && teamsPerGroupStage > 0 {
if structurePreset != .doubleGroupStage {
LabeledContent {
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
}
} else {
LabeledContent {
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
Text("Première phase")
}
LabeledContent {
let mp = (groupStageCount * (groupStageCount - 1) / 2)
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
Text("Deuxième phase")
}
LabeledContent {
let mp = groupStageCount - 1 + teamsPerGroupStage - 1
Text(mp.formatted())
} label: {
Text("Matchs à jouer par équipe")
Text("Total")
}
}
}
}
} else {
ContentUnavailableView("Erreur", systemImage: "divide.circle.fill", description: Text("Il y n'y a pas assez d'équipe pour ce nombre de poule."))
}
}
Section {
if groupStageCount > 0 {
if structurePreset != .doubleGroupStage {
LabeledContent {
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes en poule")
}
LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
} label: {
Text("Équipes qualifiées de poule")
}
}
}
if structurePreset != .doubleGroupStage {
LabeledContent {
Text(tsPure.formatted())
} label: {
Text("Équipes à placer en tableau")
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
Text("Attention !").foregroundStyle(.red)
}
}
// if groupStageCount > 0 {
// LabeledContent {
// Text(tf.formatted())
// } label: {
// Text("Effectif tableau")
// }
// }
} else {
LabeledContent {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage
Text((mp1 + mp2).formatted())
} label: {
Text("Total de matchs")
}
}
LabeledContent {
FooterButtonView("configurer") {
showSeedRepartition = true
}
} label: {
if tournament.state() == .build {
Text("Répartition des équipes")
} else if selectedTournament != nil {
Text("La configuration du tournoi séléctionné sera utilisée.")
} else {
Text(_seeds())
}
}
.onAppear {
if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil {
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}
} footer: {
if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0, tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red)
}
}
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
Section {
Toggle("Avec wildcards", isOn: $buildWildcards)
} footer: {
Text("Padel Club réservera des places pour eux dans votre liste d'inscription.")
}
}
if tournament.rounds().isEmpty && tournament.state() == .build {
Section {
RowButtonView("Ajouter un tableau", role: .destructive) {
tournament.buildBracket(minimalBracketTeamCount: 4)
}
} footer: {
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
}
}
if tournament.state() != .initial {
if seedRepartition.isEmpty == false {
RowButtonView("Modifier la répartition des équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") {
await _handleSeedRepartition()
}
}
Section {
RowButtonView("Sauver sans reconstuire l'existant") {
_saveWithoutRebuild()
}
}
Section {
RowButtonView("Reconstruire les poules", role:.destructive) {
await _save(rebuildEverything: false)
}
}
Section {
RowButtonView("Tout refaire", role: .destructive) {
await _save(rebuildEverything: true)
}
}
Section {
RowButtonView("Remise-à-zéro", role: .destructive) {
_reset()
}
}
Section {
RowButtonView("Retirer toutes les équipes de poules", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetGroupeStagePosition()
}
}
}
Section {
RowButtonView("Retirer toutes les équipes du tableau", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetBracketPosition()
}
}
}
}
}
.focused($stepperFieldIsFocused)
.onChange(of: stepperFieldIsFocused) {
if stepperFieldIsFocused {
DispatchQueue.main.async {
UIApplication.shared.sendAction(#selector(UIResponder.selectAll(_:)), to: nil, from: nil, for: nil)
}
}
}
.toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: $showSeedRepartition, content: {
NavigationStack {
HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in
self.seedRepartition = seedRepartition
}
}
})
.onChange(of: teamCount) {
if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount)
} else {
updatedElements.remove(.teamCount)
}
_verifyValueIntegrity()
}
.onChange(of: groupStageCount) {
if groupStageCount != tournament.groupStageCount {
updatedElements.insert(.groupStageCount)
} else {
updatedElements.remove(.groupStageCount)
}
if structurePreset.isFederalPreset(), groupStageCount == 0 {
teamCount = structurePreset.tableDimension()
}
_verifyValueIntegrity()
}
.onChange(of: teamsPerGroupStage) {
if teamsPerGroupStage != tournament.teamsPerGroupStage {
updatedElements.insert(.teamsPerGroupStage)
} else {
updatedElements.remove(.teamsPerGroupStage)
}
_verifyValueIntegrity()
}
.onChange(of: qualifiedPerGroupStage) {
if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage {
updatedElements.insert(.qualifiedPerGroupStage)
} else {
updatedElements.remove(.qualifiedPerGroupStage)
}
_verifyValueIntegrity()
}
.onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified {
updatedElements.insert(.groupStageAdditionalQualified)
} else {
updatedElements.remove(.groupStageAdditionalQualified)
}
_verifyValueIntegrity()
}
.toolbar {
if tournament.state() != .initial {
ToolbarItem(placement: .topBarTrailing) {
Button("Remise-à-zéro", systemImage: "trash", role: .destructive) {
confirmReset.toggle()
}
.confirmationDialog("Remise-à-zéro", isPresented: $confirmReset, titleVisibility: .visible, actions: {
Button("Tout effacer") {
_reset()
}
Button("Annuler") {
}
}, message: {
Text("Vous êtes sur le point d'effacer le tableau et les poules déjà créés et perdre les matchs et scores correspondant.")
})
}
}
ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial {
ButtonValidateView {
Task {
await _save(rebuildEverything: true)
}
}
} else {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
ButtonValidateView(role: .destructive) {
if requirements.isEmpty {
Task {
await _save(rebuildEverything: false)
}
} else {
presentRefreshStructureWarning = true
}
}
.confirmationDialog("Refaire la structure", isPresented: $presentRefreshStructureWarning, actions: {
Button("Sauver sans reconstuire l'existant") {
_saveWithoutRebuild()
}
Button("Reconstruire les poules") {
Task {
await _save(rebuildEverything: false)
}
}
Button("Tout refaire", role: .destructive) {
Task {
await _save(rebuildEverything: true)
}
}
}, message: {
ForEach(Array(requirements)) { requirement in
Text(requirement.rebuildingRequirementMessage)
}
})
}
}
}
.navigationTitle("Structure")
.navigationBarTitleDisplayMode(.inline)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
private func _seeds() -> String {
if seedRepartition.isEmpty || seedRepartition.reduce(0, +) == 0 {
return "Aucune configuration"
}
return seedRepartition.enumerated().compactMap { (index, count) in
if count > 0 {
return RoundRule.roundName(fromRoundIndex: index) + " : \(count)"
} else {
return nil
}
}.joined(separator: ", ")
}
private func _reset() {
tournament.unsortedTeams().forEach {
$0.resetPositions()
}
tournament.removeWildCards()
tournament.deleteGroupStages()
tournament.deleteStructure()
if structurePreset != .manual {
structurePreset = PadelTournamentStructurePreset.manual
} else {
_updatePreset()
}
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
dataStore.tournaments.addOrUpdate(instance: tournament)
}
private func _saveWithoutRebuild() {
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
let updateGroupStageState = teamsPerGroupStage > tournament.teamsPerGroupStage
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if updateGroupStageState {
tournament.groupStages().forEach { groupStage in
groupStage.updateGroupStageState()
}
}
_checkGroupStagesTeams()
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
dismiss()
}
private func _checkGroupStagesTeams() {
if groupStageCount == 0 {
let teams = tournament.unsortedTeams().filter({ $0.inGroupStage() })
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
}
do {
try tournament.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
}
private func _save(rebuildEverything: Bool = false) async {
_verifyValueIntegrity()
do {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if rebuildEverything {
if let selectedTournament {
tournament.matchFormat = selectedTournament.matchFormat
tournament.groupStageMatchFormat = selectedTournament.groupStageMatchFormat
tournament.loserBracketMatchFormat = selectedTournament.loserBracketMatchFormat
tournament.initialSeedRound = selectedTournament.initialSeedRound
tournament.initialSeedCount = selectedTournament.initialSeedCount
} else {
tournament.initialSeedRound = seedRepartition.firstIndex(where: { $0 > 0 }) ?? 0
tournament.initialSeedCount = seedRepartition.first(where: { $0 > 0 }) ?? 0
}
tournament.removeWildCards()
if structurePreset.hasWildcards(), buildWildcards {
tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket)
tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage)
}
tournament.deleteAndBuildEverything(preset: structurePreset)
if let selectedTournament {
let oldTournamentStart = selectedTournament.startDate
let newTournamentStart = tournament.startDate
let calendar = Calendar.current
let oldComponents = calendar.dateComponents([.hour, .minute, .second], from: oldTournamentStart)
var newComponents = calendar.dateComponents([.year, .month, .day], from: newTournamentStart)
newComponents.hour = oldComponents.hour
newComponents.minute = oldComponents.minute
newComponents.second = oldComponents.second ?? 0
if let updatedStartDate = calendar.date(from: newComponents) {
tournament.startDate = updatedStartDate
}
let allRounds = selectedTournament.rounds()
let allRoundsNew = tournament.rounds()
allRoundsNew.forEach { round in
if let pRound = allRounds.first(where: { r in
round.index == r.index
}) {
round.setData(from: pRound, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
}
}
let allGroupStages = selectedTournament.allGroupStages()
let allGroupStagesNew = tournament.allGroupStages()
allGroupStagesNew.forEach { groupStage in
if let pGroupStage = allGroupStages.first(where: { gs in
groupStage.index == gs.index
}) {
groupStage.setData(from: pGroupStage, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
}
}
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled())
}
if seedRepartition.count > 0 {
await _handleSeedRepartition()
}
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
tournament.deleteGroupStages()
tournament.buildGroupStages()
}
_checkGroupStagesTeams()
try dataStore.tournaments.addOrUpdate(instance: tournament)
dismiss()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
private func _handleSeedRepartition() async {
while tournament.rounds().count < seedRepartition.count {
await tournament.addNewRound(tournament.rounds().count)
}
if seedRepartition.reduce(0, +) > 0 {
let rounds = tournament.rounds()
let roundsToDelete = rounds.suffix(rounds.count - seedRepartition.count)
for round in roundsToDelete {
await tournament.removeRound(round)
}
}
for (index, seedCount) in seedRepartition.enumerated() {
if let round = tournament.rounds().first(where: { $0.index == index }) {
let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index)
let playedMatches = round.playedMatches().map { $0.index - baseIndex }
let allMatches = round._matches()
let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in
playedMatches.contains(index)
}).prefix(seedCount)
if playedMatches.count == numberOfMatches && seedCount == numberOfMatches * 2 {
continue
}
for (index, value) in seedSorted.enumerated() {
let isOpponentTurn = index >= playedMatches.count
if let match = allMatches[safe:value] {
if match.index - baseIndex < numberOfMatches / 2 {
if isOpponentTurn {
match.previousMatch(.two)?.disableMatch()
} else {
match.previousMatch(.one)?.disableMatch()
}
} else {
if isOpponentTurn {
match.previousMatch(.one)?.disableMatch()
} else {
match.previousMatch(.two)?.disableMatch()
}
}
}
}
if seedCount > 0 {
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches())
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches())
round.deleteLoserBracket()
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
}
}
}
private func _updatePreset() {
if let selectedTournament {
seedRepartition = []
teamCount = selectedTournament.teamCount
groupStageCount = selectedTournament.groupStageCount
teamsPerGroupStage = selectedTournament.teamsPerGroupStage
qualifiedPerGroupStage = selectedTournament.qualifiedPerGroupStage
groupStageAdditionalQualified = selectedTournament.groupStageAdditionalQualified
buildWildcards = tournament.level.wildcardArePossible()
} else {
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
groupStageCount = structurePreset.groupStageCount()
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
}
_verifyValueIntegrity()
}
private func _verifyValueIntegrity() {
if teamCount > 128 {
teamCount = 128
}
if groupStageCount > maxGroupStages {
groupStageCount = maxGroupStages
}
if teamCount < 4 {
teamCount = 4
}
if groupStageCount < 0 {
groupStageCount = 0
}
if groupStageCount > 0 {
if teamsPerGroupStage > (teamCount / groupStageCount) {
teamsPerGroupStage = (teamCount / groupStageCount)
}
if qualifiedPerGroupStage > (teamsPerGroupStage-1) {
qualifiedPerGroupStage = (teamsPerGroupStage-1)
}
if groupStageAdditionalQualified > maxMoreQualified {
groupStageAdditionalQualified = maxMoreQualified
}
if teamsPerGroupStage < 2 {
teamsPerGroupStage = 2
}
if qualifiedPerGroupStage < 0 {
qualifiedPerGroupStage = 0
}
if groupStageAdditionalQualified < 0 {
groupStageAdditionalQualified = 0
}
}
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}
extension TableStructureView {
enum StructureElement: Int, Identifiable {
case teamCount
case groupStageCount
case teamsPerGroupStage
case qualifiedPerGroupStage
case groupStageAdditionalQualified
var id: Int { self.rawValue }
var requiresRebuilding: RebuildingRequirement? {
switch self {
case .teamCount:
return .all
case .groupStageCount:
return .groupStage
case .teamsPerGroupStage:
return .groupStage
case .qualifiedPerGroupStage:
return nil
case .groupStageAdditionalQualified:
return nil
}
}
}
enum RebuildingRequirement: Int, Identifiable {
case groupStage
case all
var id: Int { self.rawValue }
var rebuildingRequirementMessage: String {
switch self {
case .groupStage:
return "Si vous le souhaitez, seulement les poules seront refaites. Le tableau ne sera pas modifié."
case .all:
return "Tous les matchs seront re-générés. La position des têtes de série sera remise à zéro et les poules seront reconstruites."
}
}
}
}
//#Preview {
// NavigationStack {
// TableStructureView()
// .environment(Tournament.mock())
// .environmentObject(DataStore.shared)
// }
//}
func frenchUmpireOrder(for matches: [Int]) -> [Int] {
if matches.count <= 1 { return matches }
if matches.count == 2 { return [matches[1], matches[0]] }
var result: [Int] = []
// Step 1: Take last, then first
result.append(matches.last!)
result.append(matches.first!)
// Step 2: Get remainder (everything except first and last)
let remainder = Array(matches[1..<matches.count-1])
if remainder.isEmpty { return result }
// Step 3: Split remainder in half
let mid = remainder.count / 2
let firstHalf = Array(remainder[0..<mid])
let secondHalf = Array(remainder[mid..<remainder.count])
// Step 4: Take first of 2nd half, then last of 1st half
if !secondHalf.isEmpty {
result.append(secondHalf.first!)
}
if !firstHalf.isEmpty {
result.append(firstHalf.last!)
}
// Step 5: Build new remainder from what's left and recurse
let newRemainder = Array(firstHalf.dropLast()) + Array(secondHalf.dropFirst())
result.append(contentsOf: frenchUmpireOrder(for: newRemainder))
return result
}
/// Convenience wrapper
func frenchUmpireOrder(for matchCount: Int) -> [Int] {
let m = frenchUmpireOrder(for: Array(0..<matchCount))
return m + m.reversed()
}