package de.patani.kettlebelltracker.viewmodels import android.media.AudioManager import android.media.ToneGenerator import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.patani.kettlebelltracker.data.local.TrainingSessionDao import de.patani.kettlebelltracker.data.datastore.SettingsDataStore import de.patani.kettlebelltracker.repositories.ApiRepository import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.Date import kotlin.math.min data class TrainingState( val isTrainingRunning: Boolean = false, val remainingSeconds: Int = 0, val initialDurationSeconds: Int = 0, val setsDone: Int = 0, val goalSets: Int = 5, val repsPerSet: Int = 5, val progress: Float = 0.0f, val currentProgram: String = "clean_1.0", val currentBlockDay: Int = 1, val currentReps: Int = 5, val totalTrainingDays: Int = 0, // Neue Rundentimer-Eigenschaften val isRoundActive: Boolean = false, val currentRoundTime: Int = 0, val totalRoundTime: Int = 0 ) class TrainingViewModel( private val dao: TrainingSessionDao, private val settingsDataStore: SettingsDataStore, private val apiRepository: ApiRepository, private val appUUID: String ) : ViewModel() { private val _trainingState = MutableStateFlow(TrainingState()) val trainingState = _trainingState.asStateFlow() private var timerJob: Job? = null private var roundTimerJob: Job? = null init { viewModelScope.launch { val trainingCount = dao.getTrainingCount().first() val initialState = calculateStateByDayCount(trainingCount) _trainingState.update { it.copy( totalTrainingDays = trainingCount, currentProgram = initialState.program, currentBlockDay = initialState.blockDay, currentReps = initialState.reps, repsPerSet = initialState.reps ) } } } private var recommendedRestSeconds: Int = 90 fun startTraining() { if (_trainingState.value.isTrainingRunning) return viewModelScope.launch { val settings = settingsDataStore.settingsFlow.first() val durationSeconds = settings.trainingTimeMinutes * 60 _trainingState.update { it.copy( isTrainingRunning = true, initialDurationSeconds = durationSeconds, remainingSeconds = durationSeconds, goalSets = settings.goalSets, setsDone = 0, progress = 0.0f ) } val recommendation = apiRepository.getRecommendedRest( uuid = appUUID, repsPerSet = _trainingState.value.repsPerSet, currentSets = 0 // ggf. 0 oder deinen Startwert ) recommendedRestSeconds = recommendation?.recommended_rest ?: 90 Log.d("Training", "using rest time ${recommendedRestSeconds.toString()}") startTimer() startFirstRound() } } private fun startTimer() { timerJob?.cancel() timerJob = viewModelScope.launch { while (_trainingState.value.remainingSeconds > 0 && _trainingState.value.isTrainingRunning) { delay(1000) _trainingState.update { it.copy(remainingSeconds = it.remainingSeconds - 1) } } if (_trainingState.value.isTrainingRunning) { finishTraining() } } } private fun startFirstRound() { startRoundTimer(recommendedRestSeconds) } fun completeSet() { if (!_trainingState.value.isTrainingRunning) return val currentState = _trainingState.value val newSetsDone = currentState.setsDone + 1 val newProgress = if (currentState.goalSets > 0) { min(newSetsDone.toFloat() / currentState.goalSets.toFloat(), 1.0f) } else 0.0f _trainingState.update { it.copy( setsDone = newSetsDone, progress = newProgress ) } stopRoundTimer() if (newSetsDone < currentState.goalSets) { startNextRound() } } private fun startNextRound() { startRoundTimer(recommendedRestSeconds) } private fun startRoundTimer(seconds: Int) { roundTimerJob?.cancel() _trainingState.update { it.copy( isRoundActive = true, currentRoundTime = seconds, totalRoundTime = seconds ) } roundTimerJob = viewModelScope.launch { while (_trainingState.value.currentRoundTime > 0 && _trainingState.value.isRoundActive) { delay(1000) _trainingState.update { it.copy(currentRoundTime = it.currentRoundTime - 1) } } if (_trainingState.value.isRoundActive) { playRoundCompleteSound() } } } private fun stopRoundTimer() { roundTimerJob?.cancel() _trainingState.update { it.copy( isRoundActive = false, currentRoundTime = 0, totalRoundTime = 0 ) } } private fun playRoundCompleteSound() { viewModelScope.launch { try { val toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100) toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 1000) } catch (e: Exception) { Log.e("TrainingViewModel", "Could not play sound", e) } } } fun finishTraining() { timerJob?.cancel() roundTimerJob?.cancel() if (!_trainingState.value.isTrainingRunning) return viewModelScope.launch { val state = _trainingState.value val settings = settingsDataStore.settingsFlow.first() val session = de.patani.kettlebelltracker.data.local.TrainingSession( date = Date(), sets = state.setsDone, weightLeft = settings.weightLeft, weightRight = settings.weightRight, repsPerSet = state.repsPerSet, duration = (state.initialDurationSeconds - state.remainingSeconds).toLong(), program = state.currentProgram, blockDay = state.currentBlockDay ) dao.insert(session) apiRepository.sendTrainingData(session, appUUID) resetTraining() } } private suspend fun resetTraining() { val trainingCount = dao.getTrainingCount().first() val nextState = calculateStateByDayCount(trainingCount) _trainingState.value = TrainingState( totalTrainingDays = trainingCount, currentProgram = nextState.program, currentBlockDay = nextState.blockDay, currentReps = nextState.reps, repsPerSet = nextState.reps ) } private fun calculateStateByDayCount(totalDays: Int): ProgramState { if (totalDays <= 0) { return ProgramState("clean_1.0", 1, 5) } val cycleIndex = (totalDays / 12) % 6 val programs = listOf("clean_1.0", "snatch_1.0", "clean_1.1", "snatch_1.1", "clean_1.2", "snatch_1.2") val program = programs[cycleIndex] val blockDay = (totalDays % 3) + 1 val repsMap = mapOf( "clean_1.0" to listOf(5, 6, 4), "clean_1.1" to listOf(6, 8, 7), "clean_1.2" to listOf(7, 9, 8), "snatch_1.0" to listOf(5, 6, 4), "snatch_1.1" to listOf(6, 8, 7), "snatch_1.2" to listOf(7, 9, 8) ) val reps = repsMap[program]?.getOrNull(blockDay - 1) ?: 5 return ProgramState(program, blockDay, reps) } override fun onCleared() { super.onCleared() timerJob?.cancel() roundTimerJob?.cancel() } data class ProgramState(val program: String, val blockDay: Int, val reps: Int) }