package com.mindmachine.mvp.session import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.mindmachine.mvp.audio.BinauralAudioEngine import com.mindmachine.mvp.audio.HeadsetMonitor import com.mindmachine.mvp.data.SettingsRepository import com.mindmachine.mvp.domain.AppSettings import com.mindmachine.mvp.domain.CountdownPreference import com.mindmachine.mvp.domain.Presets import com.mindmachine.mvp.domain.RuntimeState import com.mindmachine.mvp.domain.SessionConfig import com.mindmachine.mvp.domain.SessionMode import com.mindmachine.mvp.domain.SessionPreset import com.mindmachine.mvp.domain.toConfig import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch const val SAFETY_VERSION = 1 data class UiState( val settings: AppSettings = AppSettings(), val presets: List = Presets.builtIn, val selectedPreset: SessionPreset = Presets.builtIn.first(), val config: SessionConfig = Presets.builtIn.first().toConfig(), val runtimeState: RuntimeState = RuntimeState.IDLE, val error: String? = null, val remainingSec: Int = 0, val countdownSec: Int = 0, val endedEarly: Boolean = false, val interruptionReason: String? = null, ) class MainViewModel( private val settingsRepository: SettingsRepository, private val headsetMonitor: HeadsetMonitor, private val audioEngine: BinauralAudioEngine, ) : ViewModel() { private val _ui = MutableStateFlow(UiState()) val ui: StateFlow = _ui.asStateFlow() private var runJob: Job? = null init { viewModelScope.launch { settingsRepository.settings.collect { s -> _ui.update { current -> val preset = current.presets.find { it.id == (s.lastPresetId ?: current.selectedPreset.id) } ?: current.selectedPreset current.copy(settings = s, selectedPreset = preset, config = current.config.copy(mode = s.defaultModePreference)) } } } } fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) } fun choosePreset(id: String) = viewModelScope.launch { val preset = _ui.value.presets.first { it.id == id } settingsRepository.updateLastPreset(id) _ui.update { it.copy(selectedPreset = preset, config = preset.toConfig(it.settings.defaultModePreference), error = null) } } fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) } fun setDurationMin(min: Int) = _ui.update { it.copy(config = it.config.copy(durationSec = (min.coerceIn(1, 30) * 60)), error = null) } fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) } fun setCarrier(value: Float) = _ui.update { it.copy(config = it.config.copy(carrierFrequencyHz = value.coerceIn(80f, 400f)), error = null) } fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) } fun updateCountdown(pref: CountdownPreference) = viewModelScope.launch { settingsRepository.updateCountdown(pref) } fun updateDefaultMode(mode: SessionMode) = viewModelScope.launch { settingsRepository.updateDefaultMode(mode) } fun updateGuidance(value: Boolean) = viewModelScope.launch { settingsRepository.updateGuidance(value) } fun startSession() { val state = _ui.value if (!(state.settings.safetyAcknowledged && state.settings.safetyAcknowledgedVersion >= SAFETY_VERSION)) { _ui.update { it.copy(error = "You must acknowledge safety before starting sessions.") } return } val headset = headsetMonitor.isStereoHeadsetAvailable() val validation = SessionValidator.validate(state.config, headset) if (validation != null) { _ui.update { it.copy(error = validation) } return } runJob?.cancel() runJob = viewModelScope.launch { val count = state.settings.countdownPreference.seconds if (count > 0) { for (i in count downTo 1) { _ui.update { it.copy(runtimeState = RuntimeState.COUNTDOWN, countdownSec = i, remainingSec = state.config.durationSec, endedEarly = false, interruptionReason = null) } delay(1000) } } _ui.update { it.copy(runtimeState = RuntimeState.RUNNING, remainingSec = state.config.durationSec, countdownSec = 0, error = null) } if (state.config.mode != SessionMode.VISUAL_ONLY) { audioEngine.start(state.config.carrierFrequencyHz, state.config.binauralDifferenceHz) } var remaining = state.config.durationSec while (remaining > 0) { delay(1000) if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) { audioEngine.stop() _ui.update { it.copy(runtimeState = RuntimeState.INTERRUPTED, interruptionReason = "Headphones disconnected. Session paused.") } return@launch } remaining -= 1 _ui.update { it.copy(remainingSec = remaining) } } audioEngine.stop() _ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false) } } } fun pause(reason: String? = null) { if (_ui.value.runtimeState != RuntimeState.RUNNING) return runJob?.cancel() audioEngine.stop() _ui.update { it.copy(runtimeState = if (reason == null) RuntimeState.PAUSED else RuntimeState.INTERRUPTED, interruptionReason = reason) } } fun resume() { val state = _ui.value if (state.runtimeState != RuntimeState.PAUSED && state.runtimeState != RuntimeState.INTERRUPTED) return if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) { _ui.update { it.copy(error = "Headphones are required to resume audio mode.") } return } runJob = viewModelScope.launch { for (i in 3 downTo 1) { _ui.update { it.copy(runtimeState = RuntimeState.COUNTDOWN, countdownSec = i) } delay(1000) } _ui.update { it.copy(runtimeState = RuntimeState.RUNNING, countdownSec = 0) } if (state.config.mode != SessionMode.VISUAL_ONLY) { audioEngine.start(state.config.carrierFrequencyHz, state.config.binauralDifferenceHz) } var remaining = state.remainingSec while (remaining > 0) { delay(1000) if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) { audioEngine.stop() _ui.update { it.copy(runtimeState = RuntimeState.INTERRUPTED, interruptionReason = "Headphones disconnected. Session paused.") } return@launch } remaining -= 1 _ui.update { it.copy(remainingSec = remaining) } } audioEngine.stop() _ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false) } } } fun stop() { runJob?.cancel() audioEngine.stop() _ui.update { it.copy(runtimeState = RuntimeState.STOPPED, endedEarly = true) } } fun switchToVisualOnlyAndResume() { _ui.update { it.copy(config = it.config.copy(mode = SessionMode.VISUAL_ONLY), error = null) } resume() } override fun onCleared() { audioEngine.stop() super.onCleared() } class Factory( private val settingsRepository: SettingsRepository, private val headsetMonitor: HeadsetMonitor, private val audioEngine: BinauralAudioEngine, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T = MainViewModel(settingsRepository, headsetMonitor, audioEngine) as T } }