package com.onthelevel.core.audio

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

/**
 * Pure policy for spoken Surface leveling guidance — a SETTLED-CHANGE announcer, not a
 * timer. It talks like a person watching the bubble: ONE correction on the axis that's
 * most off, naming the low side to RAISE, softened to "a little" when close, and "that's
 * level" once.
 *
 * It speaks only when there's something new to say. While the board is MOVING it stays
 * silent (but remembers movement happened); when it SETTLES again it re-assesses and
 * speaks only if the dominant direction changed, the correction crossed coarse→fine, or
 * it reached lock. A board that settles still needing the same coarse nudge gets silence —
 * the person already has that instruction. There is no periodic repeat.
 *
 * Sign conventions (OrientationMath): pitch>0 = top edge high, roll>0 = right edge high.
 * To level you raise the LOW side, so top-high -> raise bottom, right-high -> raise left.
 */
class VoiceGuidancePolicy(
    private val fineThresholdDegrees: Double = FINE_THRESHOLD_DEGREES,
    private val dominanceMarginDegrees: Double = DOMINANCE_MARGIN_DEGREES,
) {
    enum class Direction { RAISE_LEFT, RAISE_RIGHT, RAISE_TOP, RAISE_BOTTOM }

    sealed interface Phrase {
        data class Raise(val direction: Direction, val fine: Boolean) : Phrase
        data object Level : Phrase
    }

    private var spokenDirection: Direction? = null
    private var spokenFine = false
    private var levelAnnounced = false
    // True right after movement, so the next settled frame re-assesses. Starts true so the
    // very first settled reading gives an instruction.
    private var awaitingSettledAssessment = true

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

        if (input.isLocked) {
            spokenDirection = null // re-announce direction after any unlock
            awaitingSettledAssessment = false
            if (levelAnnounced) return null
            levelAnnounced = true
            return Phrase.Level
        }
        levelAnnounced = false

        // Moving: stay silent, but note that we must re-assess once it settles. Preserve
        // the last spoken direction/tier (do NOT fully reset — that caused the repeats).
        if (input.isSettling) {
            awaitingSettledAssessment = true
            return null
        }

        // Settled and off-level: speak only on a meaningful change since the last settle.
        val direction = dominantDirection(input.pitchDegrees, input.rollDegrees)
        val fine = maxOf(abs(input.pitchDegrees), abs(input.rollDegrees)) <= fineThresholdDegrees
        val speak = awaitingSettledAssessment && (
            spokenDirection == null ||        // first instruction
            direction != spokenDirection ||   // dominant direction changed
            (fine && !spokenFine)             // crossed coarse -> fine
        )
        awaitingSettledAssessment = false
        return if (speak) {
            spokenDirection = direction
            spokenFine = fine
            Phrase.Raise(direction, fine)
        } else {
            null
        }
    }

    private fun dominantDirection(pitch: Double, roll: Double): Direction {
        val pitchDir = if (pitch > 0) Direction.RAISE_BOTTOM else Direction.RAISE_TOP
        val rollDir = if (roll > 0) Direction.RAISE_LEFT else Direction.RAISE_RIGHT
        val prev = spokenDirection
        val ambiguous = abs(abs(pitch) - abs(roll)) < dominanceMarginDegrees
        if (ambiguous && prev != null) {
            // Stay on whichever axis we're already coaching, to avoid flapping on a diagonal.
            if (prev == Direction.RAISE_TOP || prev == Direction.RAISE_BOTTOM) return pitchDir
            return rollDir
        }
        return if (abs(pitch) >= abs(roll)) pitchDir else rollDir
    }

    fun reset() {
        spokenDirection = null
        spokenFine = false
        levelAnnounced = false
        awaitingSettledAssessment = true
    }

    data class Input(
        val isEnabled: Boolean,
        val isAppForeground: Boolean,
        val placementOk: Boolean,
        val presentation: SurfacePresentation?,
        val isSettling: Boolean,
        val pitchDegrees: Double,
        val rollDegrees: Double,
        val isLocked: Boolean,
        val nowMillis: Long,
    )

    companion object {
        const val FINE_THRESHOLD_DEGREES = 1.0
        const val DOMINANCE_MARGIN_DEGREES = 0.3
    }
}
