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.
 
 
PadelClubData/PadelClubData/Data/DataStore.swift

394 lines
15 KiB

//
// DataStore.swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import LeStorage
import SwiftUI
import Combine
public class DataStore: ObservableObject {
public static let shared = DataStore()
@Published public var user: CustomUser = CustomUser.placeHolder() {
didSet {
let loggedUser = StoreCenter.main.isAuthenticated
if loggedUser {
if self.user.id != self.userStorage.item()?.id {
self.userStorage.setItemNoSync(self.user)
StoreCenter.main.initialSynchronization(clear: false)
self._fixMissingClubCreatorIfNecessary()
self._fixMissingEventCreatorIfNecessary()
}
} else {
self._temporaryLocalUser.item = self.user
}
}
}
public fileprivate(set) var tournaments: SyncedCollection<Tournament>
public fileprivate(set) var clubs: SyncedCollection<Club>
public fileprivate(set) var courts: SyncedCollection<Court>
public fileprivate(set) var events: SyncedCollection<Event>
public fileprivate(set) var monthData: StoredCollection<MonthData>
public fileprivate(set) var dateIntervals: SyncedCollection<DateInterval>
public fileprivate(set) var purchases: SyncedCollection<Purchase>
public var userStorage: StoredSingleton<CustomUser>
fileprivate var _temporaryLocalUser: OptionalStorage<CustomUser> = OptionalStorage(fileName: "tmp_local_user.json")
public fileprivate(set) var appSettingsStorage: MicroStorage<AppSettings> = MicroStorage(fileName: "appsettings.json")
public var appSettings: AppSettings {
appSettingsStorage.item
}
init() {
let store = Store.main
let indexed: Bool = true
self.clubs = store.registerSynchronizedCollection(indexed: indexed)
self.courts = store.registerSynchronizedCollection(indexed: indexed)
self.tournaments = store.registerSynchronizedCollection(indexed: indexed)
self.events = store.registerSynchronizedCollection(indexed: indexed)
self.dateIntervals = store.registerSynchronizedCollection(indexed: indexed)
self.userStorage = store.registerObject(synchronized: true, shouldLoadDataFromServer: false)
self.purchases = store.registerSynchronizedCollection(inMemory: true)
self.monthData = store.registerCollection(indexed: indexed)
// Load ApiCallCollection, making them restart at launch and deletable on disconnect
StoreCenter.main.loadApiCallCollection(type: GroupStage.self)
StoreCenter.main.loadApiCallCollection(type: Round.self)
StoreCenter.main.loadApiCallCollection(type: PlayerRegistration.self)
StoreCenter.main.loadApiCallCollection(type: TeamRegistration.self)
StoreCenter.main.loadApiCallCollection(type: Match.self)
StoreCenter.main.loadApiCallCollection(type: TeamScore.self)
StoreCenter.main.loadApiCallCollection(type: DrawLog.self)
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidUpdate), name: NSNotification.Name.CollectionDidChange, object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(_willEnterForegroundNotification),
name: UIScene.willEnterForegroundNotification,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
public func saveUser() {
if user.username.count > 0 {
self.userStorage.update()
} else {
self._temporaryLocalUser.item = self.user
}
}
@objc func collectionDidLoad(notification: Notification) {
if let userSingleton: StoredCollection<CustomUser> = notification.object as? StoredCollection<CustomUser> {
self.user = userSingleton.first ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
} else if notification.object is StoredCollection<Club> {
self._fixMissingClubCreatorIfNecessary()
} else if notification.object is StoredCollection<Event> {
self._fixMissingEventCreatorIfNecessary()
}
if Store.main.fileCollectionsAllLoaded() {
AutomaticPatcher.applyAllWhenApplicable()
self.resetOngoingCache()
}
}
fileprivate func _fixMissingClubCreatorIfNecessary() {
if self.user.clubs.count > 0 { return }
let clubsCollection = DataStore.shared.clubs
for club in clubsCollection {
if let userId = StoreCenter.main.userId, club.creator == nil {
club.creator = userId
self.userStorage.item()?.addClub(club)
self.userStorage.update()
clubsCollection.writeChangeAndInsertOnServer(instance: club)
}
}
}
fileprivate func _fixMissingEventCreatorIfNecessary() {
let eventsCollection = DataStore.shared.events
for event in eventsCollection {
if let userId = StoreCenter.main.userId, event.creator == nil {
event.creator = userId
do {
try event.insertOnServer()
} catch {
Logger.error(error)
}
}
}
}
@objc func collectionDidUpdate(notification: Notification) {
self.objectWillChange.send()
}
@objc func _willEnterForegroundNotification() {
Task {
try await self.purchases.loadDataFromServerIfAllowed(clear: true)
}
}
public func disconnect() {
Task {
if await StoreCenter.main.hasPendingAPICalls() {
// todo qu'est ce qu'on fait des API Call ?
}
do {
let services = try StoreCenter.main.service()
try await services.logout()
} catch {
Logger.error(error)
}
DispatchQueue.main.async {
self._localDisconnect()
}
}
}
public func deleteAccount() {
Task {
do {
let services = try StoreCenter.main.service()
try await services.deleteAccount()
} catch {
Logger.error(error)
}
DispatchQueue.main.async {
self._localDisconnect()
}
}
}
public func deleteTournament(_ tournament: Tournament, noSync: Bool = false) {
let event = tournament.eventObject()
let isLastTournament = event?.tournaments.count == 1
if noSync {
self.tournaments.deleteNoSync(instance: tournament, cascading: true)
if let event, isLastTournament {
self.events.deleteNoSync(instance: event, cascading: true)
}
} else {
self.tournaments.delete(instance: tournament)
if let event, isLastTournament {
self.events.delete(instance: event)
}
}
StoreCenter.main.destroyStore(identifier: tournament.id)
}
fileprivate func _localDisconnect() {
let tournamendIds: [String] = self.tournaments.map { $0.id }
TournamentLibrary.shared.reset()
self.tournaments.reset()
self.clubs.reset()
self.courts.reset()
self.events.reset()
self.dateIntervals.reset()
self.userStorage.reset()
self.purchases.reset()
Guard.main.disconnect()
StoreCenter.main.disconnect()
for tournament in tournamendIds {
StoreCenter.main.destroyStore(identifier: tournament.id)
}
self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder()
self.user.clubs.removeAll()
self.user.licenceId = nil
}
// func copyToLocalServer(tournament: Tournament) {
//
// Task {
// do {
//
// if let url = PListReader.readString(plist: "local", key: "local_server"),
// let login = PListReader.readString(plist: "local", key: "username"),
// let pass = PListReader.readString(plist: "local", key: "password") {
// let service = Services(url: url)
// let _: CustomUser = try await service.login(username: login, password: pass)
//
// tournament.event = nil
// _ = try await service.post(tournament)
//
// for groupStage in tournament.groupStages() {
// _ = try await service.post(groupStage)
// }
// for round in tournament.rounds() {
// try await self._insertRoundAndChildren(round: round, service: service)
// }
// for teamRegistration in tournament.unsortedTeams() {
// _ = try await service.post(teamRegistration)
// for playerRegistration in teamRegistration.unsortedPlayers() {
// _ = try await service.post(playerRegistration)
// }
// }
// for groupStage in tournament.groupStages() {
// for match in groupStage._matches() {
// try await self._insertMatch(match: match, service: service)
// }
// }
// for round in tournament.allRounds() {
// for match in round._matches() {
// try await self._insertMatch(match: match, service: service)
// }
// }
//
// }
// } catch {
// Logger.error(error)
// }
// }
//
// }
//
// fileprivate func _insertRoundAndChildren(round: Round, service: Services) async throws {
// _ = try await service.post(round)
// for loserRound in round.loserRounds() {
// try await self._insertRoundAndChildren(round: loserRound, service: service)
// }
// }
//
// fileprivate func _insertMatch(match: Match, service: Services) async throws {
// _ = try await service.post(match)
// for teamScore in match.teamScores {
// _ = try await service.post(teamScore)
// }
//
// }
// MARK: - Convenience
private var _lastRunningCheckDate: Date? = nil
private var _cachedRunningMatches: [Match]? = nil
public func runningMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastRunningCheckDate,
let cachedMatches = _cachedRunningMatches,
dateNow.timeIntervalSince(lastCheck) < 5 {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {
if let store = tournament.tournamentStore {
let matches = store.matches.filter { match in
match.disabled == false && match.isRunning()
}
runningMatches.append(contentsOf: matches)
}
}
_lastRunningCheckDate = dateNow
_cachedRunningMatches = runningMatches
return _cachedRunningMatches!
}
private var _lastRunningAndNextCheckDate: Date? = nil
private var _cachedRunningAndNextMatches: [Match]? = nil
public func resetOngoingCache() {
_lastEndCheckDate = nil
_lastRunningCheckDate = nil
_lastRunningAndNextCheckDate = nil
}
public func runningAndNextMatches(_ selectedTournaments: Set<String> = Set()) -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastRunningAndNextCheckDate,
let cachedMatches = _cachedRunningAndNextMatches,
dateNow.timeIntervalSince(lastCheck) < 5 {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { (selectedTournaments.isEmpty || selectedTournaments.contains($0.id) == false) && $0.isDeleted == false && $0.startDate <= dateNow.addingTimeInterval(86_400) && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {
if let store = tournament.tournamentStore {
let matches = store.matches.filter { match in
match.disabled == false && match.startDate != nil && match.endDate == nil }
runningMatches.append(contentsOf: matches)
}
}
_lastRunningAndNextCheckDate = dateNow
_cachedRunningAndNextMatches = runningMatches
return _cachedRunningAndNextMatches!
}
private var _lastEndCheckDate: Date? = nil
private var _cachedEndMatches: [Match]? = nil
public func endMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastEndCheckDate,
let cachedMatches = _cachedEndMatches,
dateNow.timeIntervalSince(lastCheck) < 5 {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {
if let store = tournament.tournamentStore {
let matches = store.matches.filter { match in
match.disabled == false && match.hasEnded() }
runningMatches.append(contentsOf: matches)
}
}
_lastEndCheckDate = dateNow
_cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending)
return _cachedEndMatches!
}
public func subscriptionUnitlyPayedTournaments(after date: Date) -> Int {
return DataStore.shared.tournaments.count(where: { tournament in
tournament.creationDate > date &&
tournament.payment == .subscriptionUnit &&
tournament.isCanceled == false &&
tournament.isDeleted == false })
}
}