package com.vgmlr.kiln
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.util.concurrent.TimeUnit
class KilnAlertWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val ctx = applicationContext
if (!KilnNotifications.allowed(ctx)) return Result.success()
val db = KilnDb.get(ctx)
val prefs = KilnPrefs.get(ctx)
val today = LocalDate.now()
for (account in db.accountDao().getAll()) {
if (!account.pmsAlert && !account.menstruationAlert) continue
val records = db.dayRecordDao().getAll(account.id)
val buttons = KilnButtonSet(db.buttonDao().getAll(account.id))
val starts = KilnCyclePredictor.periodStarts(records, buttons)
if (starts.isEmpty()) continue
val cycle = KilnCyclePredictor.avgCycleLength(starts)
if (account.pmsAlert) {
val hueOf = KilnGradientPalette.lookup(records, buttons, starts, cycle)
pms(ctx, prefs, account, today, cycle, hueOf)
}
if (account.menstruationAlert) {
menstruation(ctx, prefs, account, today, starts, cycle)
}
}
return Result.success()
}
private fun pms(
ctx: Context,
prefs: KilnPrefs,
account: KilnAccount,
today: LocalDate,
cycle: Int,
hueOf: (LocalDate) -> Float
) {
if (KilnDayStats.preMenstrualIn(today, cycle, hueOf) != 0L) return
val day = today.toEpochDay()
if (prefs.alertDay(KilnPrefs.KEY_PMS_DAY, account.id) == day) return
prefs.setAlertDay(KilnPrefs.KEY_PMS_DAY, account.id, day)
KilnNotifications.pms(ctx, account)
}
private fun menstruation(
ctx: Context,
prefs: KilnPrefs,
account: KilnAccount,
today: LocalDate,
starts: List<LocalDate>,
cycle: Int
) {
val id = account.id
val day = today.toEpochDay()
val anchor = prefs.alertDay(KilnPrefs.KEY_PERIOD_ANCHOR, id)
val due = starts.none { it == today } &&
KilnDayStats.daysToPeriod(today, starts, cycle) == 0L
if (due && anchor != day) {
prefs.setAlertDay(KilnPrefs.KEY_PERIOD_ANCHOR, id, day)
prefs.clearAlertDay(KilnPrefs.KEY_PERIOD_DISMISSED, id)
} else {
if (!account.menstruationReminder || anchor == KilnPrefs.NO_DAY) return
if (day !in (anchor + 1)..(anchor + RUN_DAYS - 1)) return
if (prefs.alertDay(KilnPrefs.KEY_PERIOD_DISMISSED, id) == anchor) return
if (starts.any { it.toEpochDay() >= anchor }) return
}
if (prefs.alertDay(KilnPrefs.KEY_PERIOD_DAY, id) == day) return
prefs.setAlertDay(KilnPrefs.KEY_PERIOD_DAY, id, day)
KilnNotifications.menstruation(ctx, account)
}
companion object {
private const val WORK_NAME = "alert_sync"
private const val ONCE_NAME = "alert_sync_once"
private const val RUN_DAYS = 5
private val ALERT_TIME: LocalTime = LocalTime.of(9, 0)
fun schedule(context: Context) {
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
PeriodicWorkRequestBuilder<KilnAlertWorker>(1, TimeUnit.DAYS)
.setInitialDelay(minutesUntilAlert(), TimeUnit.MINUTES)
.build()
)
}
fun runOnce(context: Context) {
WorkManager.getInstance(context).enqueueUniqueWork(
ONCE_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequestBuilder<KilnAlertWorker>().build()
)
}
private fun minutesUntilAlert(): Long {
val now = LocalDateTime.now()
var next = now.toLocalDate().atTime(ALERT_TIME)
if (!next.isAfter(now)) next = next.plusDays(1)
return Duration.between(now, next).toMinutes()
}
}
}