package com.vgmlr.kiln
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import kotlin.math.abs
object KilnMoodInfluence {
private const val MIN_MOOD_DAYS = 20
private const val RAMP_DAYS = 28
private const val BUCKET_DEG = 120f / 7f
private const val SMOOTH = 2L
fun weight(records: List<KilnDayRecord>, buttons: KilnButtonSet): Float {
val n = records.count { it.mood != null || buttons.hasSymptom(it) }
return ((n - MIN_MOOD_DAYS).toFloat() / RAMP_DAYS).coerceIn(0f, 1f)
}
private val DRAG = floatArrayOf(2f, 1f, 0f, 1f, 2f)
private fun drag(mood: Int): Float = DRAG.getOrElse(mood) { 0f }
private const val SYMPTOM_DRAG = 0.8f
fun hueShift(
records: List<KilnDayRecord>,
buttons: KilnButtonSet,
starts: List<LocalDate>,
cycle: Int,
date: LocalDate
): Float {
val w = weight(records, buttons)
if (w == 0f || starts.isEmpty()) return 0f
val phase = phaseOf(date, starts, cycle) ?: return 0f
var sum = 0f
var n = 0
for (r in records) {
val p = phaseOf(LocalDate.ofEpochDay(r.epochDay), starts, cycle) ?: continue
if (circularDist(p, phase, cycle) > SMOOTH) continue
r.mood?.let { sum += drag(it); n++ }
val s = buttons.symptomCount(r)
if (s > 0) {
sum += SYMPTOM_DRAG * s
n += s
}
}
if (n == 0) return 0f
val avg = sum / n
val confidence = n / (n + 3f)
return -avg * BUCKET_DEG * w * confidence
}
private fun phaseOf(date: LocalDate, starts: List<LocalDate>, cycle: Int): Long? {
val last = starts.lastOrNull { !it.isAfter(date) } ?: return null
return Math.floorMod(ChronoUnit.DAYS.between(last, date), cycle.toLong())
}
private fun circularDist(a: Long, b: Long, cycle: Int): Long {
val d = abs(a - b)
return minOf(d, cycle - d)
}
}