package com.onthelevel.core.audio

import com.onthelevel.core.sensors.SurfacePresentation
import kotlin.math.pow

/**
 * Pure scheduler for Surface proximity ticking (TONES mode). Continuous "warmer/colder":
 * a tick repeats faster as you near level — the RATE is the whole message, axis-agnostic,
 * exactly like a bubble level asks nothing of you (SOUND_DESIGN_HANDOFF.md). No settle
 * gate — it ticks at every point in the adjustment.
 *
 * Rate is exponential in the stable tilt error, clamped [MIN_RATE, MAX_RATE]; loudness is
 * constant (the caller plays every tick at the same volume — acceleration is the signal,
 * not volume). Centered reuses the real lock (LockDetector's dwell + hysteresis): on the
 * lock transition it emits [Cue.LEVEL] once, then stays silent until unlock.
 *
 * Scheduling is anchored to the last tick off a MONOTONIC clock (caller passes nowMillis).
 * The due time is recomputed EVERY update from that anchor plus the CURRENT interval, so a
 * change in tilt takes effect immediately — if you drop from 5° to 0.4°, the next tick
 * accelerates to the fast cadence instead of waiting out the stale slow interval. The anchor
 * advances by whole intervals (no drift, just a constant poll latency); if it ever falls a
 * full interval behind (a pause), it resyncs to "now" instead of firing a catch-up burst.
 * At most one tick per update. Re-entry — enabling TONES, returning FACE_UP, or unlocking —
 * ticks immediately.
 */
class SurfaceTickPolicy(
    private val nearDegrees: Double = NEAR_DEGREES,
    private val farDegrees: Double = FAR_DEGREES,
    private val maxRatePerSecond: Double = MAX_RATE,
    private val minRatePerSecond: Double = MIN_RATE,
) {
    enum class Cue { TICK, LEVEL }

    private var lastTickAtMillis: Long? = null // anchor: when the last tick actually fired
    private var wasLocked = false

    fun update(input: Input): Cue? {
        if (!input.isEnabled || !input.isAppForeground || !input.placementOk ||
            input.surfacePresentation != SurfacePresentation.FACE_UP
        ) {
            reset()
            return null
        }

        if (input.isLocked) {
            lastTickAtMillis = null // cancel any pending tick
            val justLocked = !wasLocked
            wasLocked = true
            return if (justLocked) Cue.LEVEL else null
        }
        wasLocked = false

        val now = input.nowMillis
        val anchor = lastTickAtMillis
        if (anchor == null) {
            // Re-entry: tick immediately.
            lastTickAtMillis = now
            return Cue.TICK
        }
        val interval = intervalMillis(input.errorDegrees) // current interval, recomputed each frame
        val due = anchor + interval
        if (now >= due) {
            // Advance the anchor by one whole interval (no drift). If we're a full interval
            // or more behind (a pause), resync to now so we don't fire a catch-up burst.
            lastTickAtMillis = if (now - due >= interval) now else due
            return Cue.TICK
        }
        return null
    }

    /** Ticks/second: [maxRatePerSecond] at/inside [nearDegrees], [minRatePerSecond] at/beyond [farDegrees]. */
    fun tickRatePerSecond(errorDegrees: Double): Double {
        val t = ((errorDegrees - nearDegrees) / (farDegrees - nearDegrees)).coerceIn(0.0, 1.0)
        return maxRatePerSecond * (minRatePerSecond / maxRatePerSecond).pow(t) // geometric = exponential
    }

    fun intervalMillis(errorDegrees: Double): Long = (1000.0 / tickRatePerSecond(errorDegrees)).toLong()

    fun reset() {
        lastTickAtMillis = null
        wasLocked = false
    }

    data class Input(
        val isEnabled: Boolean,
        val isAppForeground: Boolean,
        val placementOk: Boolean,
        val surfacePresentation: SurfacePresentation?,
        val errorDegrees: Double,
        val isLocked: Boolean,
        val nowMillis: Long,
    )

    companion object {
        const val NEAR_DEGREES = 0.4
        const val FAR_DEGREES = 5.0
        const val MAX_RATE = 8.0
        const val MIN_RATE = 1.5
    }
}
