package com.adlerosn.brasilfurfest.schedule.managers import android.content.Context import android.os.FileObserver import android.util.Log import com.adlerosn.brasilfurfest.helper.* import com.adlerosn.brasilfurfest.helper.observables.Observable import com.adlerosn.brasilfurfest.helper.observables.Observer import com.adlerosn.brasilfurfest.notification.NotificationFirer import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.Announcement import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.Convention import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.ConventionSeries import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.Event import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.managed.AttendeeConFavorites import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.managed.AttendeeConsFavorites import com.google.gson.GsonBuilder import org.jetbrains.anko.doAsync import java.io.File import java.io.Serializable import java.util.* class ScheduleManager(context: Context) : UncomplicatedObservable(), Observer, Any?>, Serializable { override fun update(observable: Observable, args: Any?) = onAttendeeIntentionsChanged() private val uniqueIdentifierFile = File(context.filesDir, "rduid.txt") val uniqueIdentifier: String val cacheManager = CacheManager(context) private val cachedChoices = File(context.filesDir, "schedule_choices.json") private val cachedChoicesCanonicalPath = cachedChoices.canonicalPath private val fireNotificationsClosure = { NotificationFirer().fire(context) } private inner class InnerFileObserver(path: String, private val closure: ()->Any?) : FixedFileObserver(path){ override fun onEvent(event: Int, path: String) { val mask = FileObserver.CLOSE_WRITE if ((event and mask) != 0) { closure() } } } private var diskChangesToNotLoad: Int = 0 private fun incrementDiskChangesToNotLoad(howMany: Int = 1){ synchronized(::diskChangesToNotLoad) { diskChangesToNotLoad+=howMany } } private fun notifyDiskChanges() { synchronized(::diskChangesToNotLoad) { if(diskChangesToNotLoad > 0) diskChangesToNotLoad-=1 else loadFromDisk() } setChanged() notifyObservers() } private val fileObserver: InnerFileObserver private val scheduleObserver: InnerFileObserver fun updateConventionSeries() = cacheManager[RemoteAssets.json.lastPathPart()]!!.first .let { ConventionJsonReader.readFromInputStream(it) } .also { conventionSeries = it } var conventionSeries: ConventionSeries = updateConventionSeries() val convention get() = conventionSeries.featured val attendeeConventionsFavorites = AttendeeConsFavorites() val attendeeFavorites get(): AttendeeConFavorites { lateinit var favorites: AttendeeConFavorites synchronized(::attendeeConventionsFavorites) { favorites = attendeeConventionsFavorites[convention] } return favorites } val conventionTime get() = GregorianCalendar().apply { timeInMillis = System.currentTimeMillis() } // .GregorianCalendar(2018,7,19,13,15) private val jsonSerializer get() = GsonBuilder() //.setExclusionStrategies(ObservableFieldsExclusionStrategy()) .registerTypeAdapter(Convention::class.java, ConventionInstanceCreator(convention)) .create() // val nextNotificationTime: Long get(){ // val early: Long = convention.notificationFireMinutesBefore*60*1000.toLong() // val now = conventionTime // val thisRange = now.dayRange.split(convention.splitDayIn).first { conventionTime in it } // val diff = thisRange.finish.timeInMillis - thisRange.start.timeInMillis // val nextEnd = thisRange.start.timeInMillis+(1*diff) // val nextNextEnd = thisRange.start.timeInMillis+(2*diff) // val earlyNextEnd = nextEnd-early // val earlyNextNextEnd = nextNextEnd-early // return if(now.timeInMillis < earlyNextEnd) earlyNextEnd else earlyNextNextEnd // } init { loadFromDisk() if(!cachedChoices.exists()) saveToDisk(false) uniqueIdentifier = initUniqueIdentifierFile() fileObserver = (InnerFileObserver(cachedChoicesCanonicalPath) {notifyDiskChanges()}).apply { startWatching() } scheduleObserver = (InnerFileObserver(cacheManager.getFileFor(RemoteAssets.json).absolutePath) {notifyScheduleChanged()}).apply { startWatching() } } private fun notifyScheduleChanged() { conventionSeries = updateConventionSeries() setChanged() notifyObservers() fireNotificationsClosure() } private val readAnnouncementsFile = File(context.filesDir, "announcements_read.txt") val unreadAnnouncements: List get() { if(!readAnnouncementsFile.exists()) return listOf() val readAnnouncementUUIDs = readAnnouncementsFile.readLines() return conventionSeries.announcements.filter { it.uuid !in readAnnouncementUUIDs } } fun setAnnouncementsRead() { readAnnouncementsFile.writeText( conventionSeries.announcements.map { it.uuid }.joinToString(separator = "\n") ) } private fun initUniqueIdentifierFile(): String{ if(!uniqueIdentifierFile.exists()) uniqueIdentifierFile.writeText(UUID.randomUUID().toString()) return uniqueIdentifierFile.readText() } private fun readObject(ois: java.io.ObjectInputStream){ ois.defaultReadObject() fileObserver.stopWatching() fileObserver.startWatching() scheduleObserver.stopWatching() scheduleObserver.startWatching() loadFromDisk() } private fun loadFromDisk() { if (cachedChoices.exists()) { synchronized(::attendeeConventionsFavorites) { val json = cachedChoices.readText() val stateFromDisk = try { jsonSerializer.fromJson(json, attendeeConventionsFavorites::class.java) } catch (e: com.google.gson.JsonSyntaxException) { Log.e("Schedule manager", "Malformed JSON", e) AttendeeConsFavorites() } if (stateFromDisk != null) { stateFromDisk.recursivelyRemoveObservers() attendeeConventionsFavorites.recursivelyRemoveObservers() attendeeConventionsFavorites.clear() attendeeConventionsFavorites.putAll(stateFromDisk) attendeeConventionsFavorites.observeAllChildren() attendeeConventionsFavorites.addObserver(this) } } } } private fun saveToDisk(async: Boolean = true) { synchronized(::attendeeConventionsFavorites) { attendeeConventionsFavorites.recursivelyRemoveObservers() val json = jsonSerializer.toJson(attendeeConventionsFavorites) attendeeConventionsFavorites.observeAllChildren() attendeeConventionsFavorites.addObserver(this) incrementDiskChangesToNotLoad() val closure = { cachedChoices.bufferedWriter().use { it.write(json) } } if(async) doAsync { closure() } else closure() } } fun onAttendeeIntentionsChanged() { saveToDisk() } fun clearIntentions() { attendeeConventionsFavorites.clear() saveToDisk() } val currentSplashAsset: String get() = ( conventionSeries.banners.firstOrNull { conventionTime in it.lifetime }?.banner ?: conventionSeries.defaultBanner ).solved!! val editionHasStarted: Boolean get() = conventionTime.timeInMillis > convention.officialLifespan.start.timeInMillis val editionHasEnded: Boolean get() = conventionTime.timeInMillis > convention.officialLifespan.finish.timeInMillis val nextEditionStartTime : Long? get(){ val currentTime = conventionTime.timeInMillis val timeUntilThis = convention.officialLifespan.start.timeInMillis - currentTime val timeUntilNext = (convention.nextEdition?.timeInMillis ?: Long.MAX_VALUE) - currentTime val stillHappening = editionHasStarted and !editionHasEnded return when { timeUntilThis > 0 -> convention.officialLifespan.start.timeInMillis stillHappening -> null timeUntilNext > 0 -> convention.nextEdition?.timeInMillis else -> null } } val nextEditionStartCountdown : Triple? get() = nextEditionStartTime?.let { buildDayHourMinuteCountdownTriple(it - conventionTime.timeInMillis) } private fun buildDayHourMinuteCountdownTriple(millisUntil: Long): Triple{ return (millisUntil/1000).let { Triple((it/(3600*24)).toInt(), ((it/3600)%24).toInt(), ((it/60)%60).toInt()) } } fun canAttend(event: Event) = attendeeFavorites.canAttend(event) }