// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
// SPDX-License-Identifier: GPL-3.0-or-later

package space.rcmd.android.sizzle.input

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
 * A tiny message bus. Every input source calls [dispatch]; the app's ViewModel
 * collects [actions] and reacts. Using a flow (rather than a direct callback)
 * decouples the sources from the UI and lets us buffer bursts of MIDI events
 * without dropping them.
 *
 * When "MIDI learn" mode is on, controls that aren't bound to anything are still
 * forwarded as [InputAction.RawControl] so the settings screen can capture them.
 */
class InputRouter(
    /** Dispatcher the hold-to-repeat loop runs on. Defaults to a background pool;
     *  tests inject a virtual-time dispatcher to drive the accelerating schedule. */
    repeatDispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
    // extraBufferCapacity gives us headroom for fast MIDI/gamepad bursts.
    private val _actions = MutableSharedFlow<InputAction>(extraBufferCapacity = 128)
    val actions: SharedFlow<InputAction> = _actions.asSharedFlow()

    /** True while the settings screen is waiting to learn a new binding. */
    @Volatile var learnMode: Boolean = false

    /** Which device kind is currently being learned, so only that source captures
     *  the next control (a keyboard press doesn't hijack a gamepad-learn, etc.). */
    @Volatile var learnSource: InputSource? = null

    fun dispatch(action: InputAction) {
        // tryEmit never suspends; if the buffer is somehow full we drop the
        // oldest by design (a missed nav key is harmless, a stall is not).
        _actions.tryEmit(action)
    }

    // ---- hold-to-repeat for navigation / value-step actions ----
    // One accelerating repeater shared by every hardware source: a source calls
    // [pressRepeat] when a control goes down and [releaseRepeat] when it comes up,
    // keyed by that control's id. Only one repeat runs at a time (holding a new nav
    // control replaces the old, which matches how you'd use it), so a single job is
    // enough. Runs off the main thread; [dispatch] is thread-safe.
    private val repeatScope = CoroutineScope(SupervisorJob() + repeatDispatcher)
    @Volatile private var repeatJob: Job? = null
    @Volatile private var repeatKey: Any? = null

    /**
     * Fire [action] once for a physical press and, if it [isRepeatable], keep
     * re-firing it while held — after [REPEAT_START_MS], accelerating by
     * [REPEAT_DECAY] each step down to [REPEAT_MIN_MS] — until [releaseRepeat] is
     * called with the same [key]. [key] identifies the physical control (a key-code,
     * a gamepad control code, a MIDI CC number) so the matching release stops it.
     */
    fun pressRepeat(key: Any, action: InputAction) {
        dispatch(action)
        if (!action.isRepeatable) return
        repeatJob?.cancel()
        repeatKey = key
        repeatJob = repeatScope.launch {
            var interval = REPEAT_START_MS
            while (isActive) {
                delay(interval)
                dispatch(action)
                interval = (interval * REPEAT_DECAY).toLong().coerceAtLeast(REPEAT_MIN_MS)
            }
        }
    }

    /** Stop the auto-repeat [pressRepeat] started for [key]. No-op if [key] isn't the
     *  one currently repeating (e.g. releasing a modifier, or a non-repeating key). */
    fun releaseRepeat(key: Any) {
        if (repeatKey == key) {
            repeatJob?.cancel()
            repeatJob = null
            repeatKey = null
        }
    }

    companion object {
        private const val REPEAT_START_MS = 320L   // initial hold before the first repeat
        private const val REPEAT_MIN_MS = 45L      // fastest repeat once accelerated
        private const val REPEAT_DECAY = 0.80      // interval shrink factor per repeat
    }
}
