package com.onthelevel.core.audio

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

/**
 * Pure scheduler for Surface TONES. One audible vocabulary: faster ticks mean "approaching",
 * and the bullseye sound means "you are centered right now" (SOUND_DESIGN_HANDOFF.md + the
 * three-way audio consensus).
 *
 * Two states, split by TIMING not by sound identity:
 *  - Outside the center zone: proximity ticks whose rate rises as you near level (exponential,
 *    clamped). Continuous "warmer/colder"; the rate is the message.
 *  - Inside the center zone: emit [Cue.ALIGNED] every frame so the caller holds the bullseye
 *    sound. Entry is IMMEDIATE — no dwell, no velocity gate — so even a fast pass over center
 *    is announced (that's the locating cue). Ticks are suppressed while aligned.
 *
 * Alignment uses SPATIAL hysteresis (enter ≤ [alignEnterDegrees], rearm only after leaving
 * ≥ [alignExitDegrees]) — not a time debounce, which would hide a legitimate quick re-crossing.
 * The error is the same hypot(pitch, roll) that drives the bubble, so the sound can't lag the
 * visual. The stronger "held level" confirmation (lime/label/haptic) is decided separately by
 * the velocity-aware LockDetector; this policy does not gate that.
 */
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,
    private val alignEnterDegrees: Double = ALIGN_ENTER_DEGREES,
    private val alignExitDegrees: Double = ALIGN_EXIT_DEGREES,
) {
    enum class Cue { TICK, ALIGNED }

    private var lastTickAtMillis: Long? = null
    private var aligned = false

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

        // Spatial hysteresis: enter the zone at the tight threshold, rearm only after
        // swinging back out past the loose one. No time debounce.
        if (aligned) {
            if (input.errorDegrees >= alignExitDegrees) aligned = false
        } else {
            if (input.errorDegrees <= alignEnterDegrees) aligned = true
        }

        if (aligned) {
            lastTickAtMillis = null // so ticks resume immediately the moment we exit
            return Cue.ALIGNED
        }

        // Outside the zone: proximity ticks, anchored to the last tick, due-time recomputed
        // every frame from the current interval (immediate reaction to changing tilt; no drift,
        // no catch-up burst). At most one tick per update.
        val now = input.nowMillis
        val anchor = lastTickAtMillis
        if (anchor == null) {
            lastTickAtMillis = now // re-entry / just exited the zone: tick immediately
            return Cue.TICK
        }
        val interval = intervalMillis(input.errorDegrees)
        val due = anchor + interval
        if (now >= due) {
            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
        aligned = false
    }

    data class Input(
        val isEnabled: Boolean,
        val isAppForeground: Boolean,
        val placementOk: Boolean,
        val surfacePresentation: SurfacePresentation?,
        val errorDegrees: Double,
        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
        const val ALIGN_ENTER_DEGREES = 0.2
        const val ALIGN_EXIT_DEGREES = 0.35
    }
}
