diff --git a/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/dashboard/components/MissionResultCard.kt b/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/dashboard/components/MissionResultCard.kt index 2b78f55..a4b5d7c 100644 --- a/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/dashboard/components/MissionResultCard.kt +++ b/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/dashboard/components/MissionResultCard.kt @@ -46,8 +46,10 @@ import java.util.Locale /** * Card component that displays the result of a completed rover mission. * Shows final position, success status, and timestamp with expandable original input. + * + * @param missionResult The mission result data to display + * @param modifier Optional modifier for styling */ -@Suppress("CyclomaticComplexMethod") @Composable fun MissionResultCard( missionResult: MissionResult, @@ -55,63 +57,17 @@ fun MissionResultCard( ) { val dateFormatter = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()) val formattedDate = dateFormatter.format(Date(missionResult.timestamp)) - var isInputExpanded by remember { mutableStateOf(false) } MarsCard( title = stringResource(R.string.mission_result), modifier = modifier, contentDescription = stringResource(R.string.cd_mission_card) ) { - // Status row with icon - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = - if (missionResult.isSuccess) { - Icons.Default.CheckCircle - } else { - Icons.Default.Warning - }, - contentDescription = - if (missionResult.isSuccess) { - stringResource(R.string.cd_mission_success) - } else { - stringResource(R.string.cd_mission_failed) - }, - tint = - if (missionResult.isSuccess) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.error - }, - modifier = Modifier.size(24.dp) - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = - if (missionResult.isSuccess) { - stringResource(R.string.mission_completed_successfully) - } else { - stringResource(R.string.mission_failed) - }, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = - if (missionResult.isSuccess) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.error - } - ) - } + MissionStatusRow(isSuccess = missionResult.isSuccess) Spacer(modifier = Modifier.height(6.dp)) - // Final position + // Final position and mission details Column( verticalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -124,60 +80,7 @@ fun MissionResultCard( // Show original input if available missionResult.originalInput?.let { input -> Spacer(modifier = Modifier.height(8.dp)) - - // Input label with expand/collapse button - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.mission_instructions), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - - if (input.length > Constants.UI.MAX_INPUT_PREVIEW_LENGTH) { - IconButton( - onClick = { isInputExpanded = !isInputExpanded }, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = if (isInputExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, - contentDescription = - if (isInputExpanded) { - stringResource( - R.string.cd_collapse_input - ) - } else { - stringResource(R.string.cd_expand_input) - }, - tint = MaterialTheme.colorScheme.primary - ) - } - } - } - - // Input text - expandable - Text( - text = - if (isInputExpanded || input.length <= Constants.UI.MAX_INPUT_PREVIEW_LENGTH) { - input - } else { - input.take(Constants.UI.MAX_INPUT_PREVIEW_LENGTH) + "..." - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = if (isInputExpanded) Int.MAX_VALUE else 2, - overflow = if (isInputExpanded) TextOverflow.Visible else TextOverflow.Ellipsis, - modifier = - if (input.length > Constants.UI.MAX_INPUT_PREVIEW_LENGTH) { - Modifier.clickable { isInputExpanded = !isInputExpanded } - } else { - Modifier - } - ) + ExpandableInputSection(input = input) } // Completed timestamp - right aligned, placed after mission instructions @@ -193,6 +96,133 @@ fun MissionResultCard( } } +/** + * Displays the mission status (success/failure) with appropriate icon and text. + * + * @param isSuccess Whether the mission was successful + * @param modifier Optional modifier for styling + */ +@Composable +private fun MissionStatusRow( + isSuccess: Boolean, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth() + ) { + Icon( + imageVector = + if (isSuccess) { + Icons.Default.CheckCircle + } else { + Icons.Default.Warning + }, + contentDescription = + if (isSuccess) { + stringResource(R.string.cd_mission_success) + } else { + stringResource(R.string.cd_mission_failed) + }, + tint = + if (isSuccess) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + }, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = + if (isSuccess) { + stringResource(R.string.mission_completed_successfully) + } else { + stringResource(R.string.mission_failed) + }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = + if (isSuccess) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + ) + } +} + +/** + * Displays mission input instructions with expand/collapse functionality for long inputs. + * + * @param input The input string to display + * @param modifier Optional modifier for styling + */ +@Composable +private fun ExpandableInputSection( + input: String, + modifier: Modifier = Modifier, +) { + var isInputExpanded by remember { mutableStateOf(false) } + + Column(modifier = modifier) { + // Input label with expand/collapse button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.mission_instructions), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + + // Show expand/collapse button only for long inputs + if (input.length > Constants.UI.MAX_INPUT_PREVIEW_LENGTH) { + IconButton( + onClick = { isInputExpanded = !isInputExpanded }, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = if (isInputExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = + if (isInputExpanded) { + stringResource(R.string.cd_collapse_input) + } else { + stringResource(R.string.cd_expand_input) + }, + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + + // Input text - expandable for long content + Text( + text = + if (isInputExpanded || input.length <= Constants.UI.MAX_INPUT_PREVIEW_LENGTH) { + input + } else { + input.take(Constants.UI.MAX_INPUT_PREVIEW_LENGTH) + "..." + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = if (isInputExpanded) Int.MAX_VALUE else 2, + overflow = if (isInputExpanded) TextOverflow.Visible else TextOverflow.Ellipsis, + modifier = + if (input.length > Constants.UI.MAX_INPUT_PREVIEW_LENGTH) { + Modifier.clickable { isInputExpanded = !isInputExpanded } + } else { + Modifier + } + ) + } +} + @Preview(name = "Mission Result Card - Success", showBackground = true) @Composable private fun MissionResultCardSuccessPreview() { diff --git a/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionScreen.kt b/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionScreen.kt index 7dd4d52..77acfd7 100644 --- a/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionScreen.kt +++ b/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionScreen.kt @@ -207,7 +207,6 @@ fun NewMissionScreen( } @OptIn(ExperimentalMaterial3Api::class) -@Suppress("CyclomaticComplexMethod") @Composable internal fun NewMissionContent( uiState: NewMissionUiState, @@ -236,7 +235,55 @@ internal fun NewMissionContent( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(24.dp) ) { - // Input mode selector + InputModeSelector( + selectedMode = uiState.inputMode, + onModeChange = onInputModeChange + ) + + InputContentSection( + uiState = uiState, + onJsonInputChange = onJsonInputChange, + onPlateauWidthChange = onPlateauWidthChange, + onPlateauHeightChange = onPlateauHeightChange, + onRoverStartXChange = onRoverStartXChange, + onRoverStartYChange = onRoverStartYChange, + onRoverDirectionChange = onRoverDirectionChange, + onMovementCommandsChange = onMovementCommandsChange, + onPlateauWidthFocusLost = onPlateauWidthFocusLost, + onPlateauHeightFocusLost = onPlateauHeightFocusLost, + onRoverStartXFocusLost = onRoverStartXFocusLost, + onRoverStartYFocusLost = onRoverStartYFocusLost, + onMovementCommandsFocusLost = onMovementCommandsFocusLost + ) + + Spacer(modifier = Modifier.height(8.dp)) + + ActionButtonsSection( + uiState = uiState, + onExecuteMission = onExecuteMission, + onNavigateBack = onNavigateBack + ) + } +} + +/** + * Segmented control for switching between JSON input and form builder input modes. + * + * @param selectedMode Currently selected input mode + * @param onModeChange Callback when input mode changes + * @param modifier Optional modifier for styling + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun InputModeSelector( + selectedMode: InputMode, + onModeChange: (InputMode) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { Text( text = stringResource(R.string.input_mode_selector_title), style = MaterialTheme.typography.titleMedium, @@ -248,8 +295,8 @@ internal fun NewMissionContent( modifier = Modifier.fillMaxWidth() ) { SegmentedButton( - selected = uiState.inputMode == InputMode.JSON, - onClick = { onInputModeChange(InputMode.JSON) }, + selected = selectedMode == InputMode.JSON, + onClick = { onModeChange(InputMode.JSON) }, shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), colors = SegmentedButtonDefaults.colors( @@ -263,8 +310,8 @@ internal fun NewMissionContent( } SegmentedButton( - selected = uiState.inputMode == InputMode.BUILDER, - onClick = { onInputModeChange(InputMode.BUILDER) }, + selected = selectedMode == InputMode.BUILDER, + onClick = { onModeChange(InputMode.BUILDER) }, shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), colors = SegmentedButtonDefaults.colors( @@ -277,62 +324,103 @@ internal fun NewMissionContent( Text(stringResource(R.string.input_mode_builder_short)) } } + } +} - // Input content based on mode - when (uiState.inputMode) { - InputMode.JSON -> { - JsonInputSection( - jsonInput = uiState.jsonInput, - jsonError = uiState.jsonError, - onJsonInputChange = onJsonInputChange - ) - } - - InputMode.BUILDER -> { - BuilderInputsForm( - uiState = uiState, - onPlateauWidthChange = onPlateauWidthChange, - onPlateauHeightChange = onPlateauHeightChange, - onRoverStartXChange = onRoverStartXChange, - onRoverStartYChange = onRoverStartYChange, - onRoverDirectionChange = onRoverDirectionChange, - onMovementCommandsChange = onMovementCommandsChange, - onPlateauWidthFocusLost = onPlateauWidthFocusLost, - onPlateauHeightFocusLost = onPlateauHeightFocusLost, - onRoverStartXFocusLost = onRoverStartXFocusLost, - onRoverStartYFocusLost = onRoverStartYFocusLost, - onMovementCommandsFocusLost = onMovementCommandsFocusLost - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - // Execute button - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - MarsButton( - text = stringResource(R.string.action_execute), - onClick = onExecuteMission, - variant = MarsButtonVariant.Primary, - isLoading = uiState.isLoading, - modifier = Modifier.weight(1f), - contentDescription = stringResource(R.string.cd_execute_mission) +/** + * Renders the appropriate input content based on the selected input mode. + * Switches between JSON text input and form builder UI. + * + * @param uiState Current UI state containing input mode and values + * @param onJsonInputChange Callback for JSON text changes + * @param modifier Optional modifier for styling + */ +@Composable +private fun InputContentSection( + uiState: NewMissionUiState, + onJsonInputChange: (String) -> Unit, + onPlateauWidthChange: (String) -> Unit, + onPlateauHeightChange: (String) -> Unit, + onRoverStartXChange: (String) -> Unit, + onRoverStartYChange: (String) -> Unit, + onRoverDirectionChange: (String) -> Unit, + onMovementCommandsChange: (String) -> Unit, + onPlateauWidthFocusLost: () -> Unit, + onPlateauHeightFocusLost: () -> Unit, + onRoverStartXFocusLost: () -> Unit, + onRoverStartYFocusLost: () -> Unit, + onMovementCommandsFocusLost: () -> Unit, + modifier: Modifier = Modifier, +) { + when (uiState.inputMode) { + InputMode.JSON -> { + JsonInputSection( + jsonInput = uiState.jsonInput, + jsonError = uiState.jsonError, + onJsonInputChange = onJsonInputChange, + modifier = modifier ) + } - MarsButton( - text = stringResource(R.string.action_cancel), - onClick = onNavigateBack, - variant = MarsButtonVariant.Secondary, - enabled = !uiState.isLoading, - contentDescription = stringResource(R.string.cd_cancel_mission) + InputMode.BUILDER -> { + BuilderInputsForm( + uiState = uiState, + onPlateauWidthChange = onPlateauWidthChange, + onPlateauHeightChange = onPlateauHeightChange, + onRoverStartXChange = onRoverStartXChange, + onRoverStartYChange = onRoverStartYChange, + onRoverDirectionChange = onRoverDirectionChange, + onMovementCommandsChange = onMovementCommandsChange, + onPlateauWidthFocusLost = onPlateauWidthFocusLost, + onPlateauHeightFocusLost = onPlateauHeightFocusLost, + onRoverStartXFocusLost = onRoverStartXFocusLost, + onRoverStartYFocusLost = onRoverStartYFocusLost, + onMovementCommandsFocusLost = onMovementCommandsFocusLost, + modifier = modifier ) } } } +/** + * Action buttons section with Execute and Cancel buttons. + * Execute button shows loading state during mission execution. + * + * @param uiState Current UI state for loading and button states + * @param onExecuteMission Callback to start mission execution + * @param onNavigateBack Callback to cancel and navigate back + * @param modifier Optional modifier for styling + */ +@Composable +private fun ActionButtonsSection( + uiState: NewMissionUiState, + onExecuteMission: () -> Unit, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + MarsButton( + text = stringResource(R.string.action_execute), + onClick = onExecuteMission, + variant = MarsButtonVariant.Primary, + isLoading = uiState.isLoading, + modifier = Modifier.weight(1f), + contentDescription = stringResource(R.string.cd_execute_mission) + ) + + MarsButton( + text = stringResource(R.string.action_cancel), + onClick = onNavigateBack, + variant = MarsButtonVariant.Secondary, + enabled = !uiState.isLoading, + contentDescription = stringResource(R.string.cd_cancel_mission) + ) + } +} + @Composable private fun JsonInputSection( jsonInput: String, diff --git a/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionViewModel.kt b/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionViewModel.kt index ecf277c..e66e08e 100644 --- a/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionViewModel.kt +++ b/app/src/main/kotlin/com/mustalk/seat/marsrover/presentation/ui/mission/NewMissionViewModel.kt @@ -11,6 +11,7 @@ import com.mustalk.seat.marsrover.domain.error.RoverError import com.mustalk.seat.marsrover.domain.usecase.ExecuteNetworkMissionUseCase import com.mustalk.seat.marsrover.domain.usecase.ExecuteRoverMissionUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -27,8 +28,8 @@ import javax.inject.Inject * - JSON input mode: Uses local execution (ExecuteRoverMissionUseCase) * - Builder input mode: Uses network API execution (ExecuteNetworkMissionUseCase) */ +@Suppress("TooManyFunctions") // Acceptable for ViewModel with proper delegation @HiltViewModel -@Suppress("TooManyFunctions") class NewMissionViewModel @Inject constructor( @@ -38,330 +39,403 @@ class NewMissionViewModel private val _uiState = MutableStateFlow(NewMissionUiState()) val uiState: StateFlow = _uiState.asStateFlow() - /** - * Switches between JSON and Builder input modes - */ - fun switchInputMode(mode: InputMode) { - _uiState.value = - _uiState.value.copy( - inputMode = mode, - errorMessage = null, - successMessage = null, - jsonError = null - ) - } - - /** - * Updates the JSON input string - */ - fun updateJsonInput(json: String) { - _uiState.value = - _uiState.value.copy( - jsonInput = json, - jsonError = null, - errorMessage = null - ) - } + private val stateManager = + StateManager( + updateState = { _uiState.value = it }, + getCurrentState = { _uiState.value } + ) - /** - * Updates individual plateau width field - */ - fun updatePlateauWidth(width: String) { - _uiState.value = - _uiState.value.copy( - plateauWidth = width, - plateauWidthError = null, - errorMessage = null - ) - } + private val fieldUpdateHandler = + FieldUpdateHandler( + updateState = { _uiState.value = it }, + getCurrentState = { _uiState.value } + ) - /** - * Updates individual plateau height field - */ - fun updatePlateauHeight(height: String) { - _uiState.value = - _uiState.value.copy( - plateauHeight = height, - plateauHeightError = null, - errorMessage = null - ) - } + private val validationHandler = + ValidationHandler( + updateState = { _uiState.value = it }, + getCurrentState = { _uiState.value } + ) - /** - * Updates rover start X position - */ - fun updateRoverStartX(x: String) { - _uiState.value = - _uiState.value.copy( - roverStartX = x, - roverStartXError = null, - errorMessage = null - ) - } + private val missionExecutor = + MissionExecutor( + executeRoverMissionUseCase = executeRoverMissionUseCase, + executeNetworkMissionUseCase = executeNetworkMissionUseCase, + stateManager = stateManager, + getCurrentState = { _uiState.value }, + scope = viewModelScope + ) - /** - * Updates rover start Y position - */ - fun updateRoverStartY(y: String) { - _uiState.value = - _uiState.value.copy( - roverStartY = y, - roverStartYError = null, - errorMessage = null - ) - } + // Primary ViewModel functions /** - * Updates rover start direction + * Switches between JSON and Builder input modes. + * Clears any existing error messages when switching modes. */ - fun updateRoverStartDirection(direction: String) { - _uiState.value = - _uiState.value.copy( - roverStartDirection = direction, - roverStartDirectionError = null, - errorMessage = null - ) - } + fun switchInputMode(mode: InputMode) = stateManager.switchInputMode(mode) /** - * Updates movement commands + * Clears all success and error messages from the UI state. */ - fun updateMovementCommands(commands: String) { - _uiState.value = - _uiState.value.copy( - movementCommands = commands, - movementCommandsError = null, - errorMessage = null - ) - } + fun clearMessages() = stateManager.clearMessages() /** - * Executes the mission based on input mode: - * - JSON mode: Local execution - * - Builder mode: Network API execution + * Executes the mission based on the current input mode: + * - JSON mode: Uses local execution with ExecuteRoverMissionUseCase + * - Builder mode: Uses network simulation with ExecuteNetworkMissionUseCase */ fun executeMission() { viewModelScope.launch { - setLoadingState() - - try { - when (_uiState.value.inputMode) { - InputMode.JSON -> executeJsonMission() - InputMode.BUILDER -> executeBuilderMission() - } - } catch (e: JsonParsingException) { - setErrorState("JSON parsing error: ${e.message}") - } catch (e: MissionExecutionException) { - setErrorState("Mission execution error: ${e.message}") - } catch (e: NetworkException) { - setErrorState("Network error: ${e.message}") - } catch (e: NumberFormatException) { - setErrorState("Invalid number format in input fields") - } catch (e: IllegalArgumentException) { - setErrorState("Invalid input parameters: ${e.message}") - } + missionExecutor.executeMission() } } - /** - * Executes mission using JSON input mode (local execution). - */ - private suspend fun executeJsonMission() { - val result = executeRoverMissionUseCase(_uiState.value.jsonInput) - - result.fold( - onSuccess = { finalPosition -> - setSuccessState("Mission completed! Final position: $finalPosition") - }, - onFailure = { error -> - val errorMessage = mapRoverErrorToMessage(error) - setErrorState(errorMessage) - } + // Field update functions - delegated to handler + fun updateJsonInput(json: String) = fieldUpdateHandler.updateJsonInput(json) + + fun updatePlateauWidth(width: String) = fieldUpdateHandler.updatePlateauWidth(width) + + fun updatePlateauHeight(height: String) = fieldUpdateHandler.updatePlateauHeight(height) + + fun updateRoverStartX(x: String) = fieldUpdateHandler.updateRoverStartX(x) + + fun updateRoverStartY(y: String) = fieldUpdateHandler.updateRoverStartY(y) + + fun updateRoverStartDirection(direction: String) = fieldUpdateHandler.updateRoverStartDirection(direction) + + fun updateMovementCommands(commands: String) = fieldUpdateHandler.updateMovementCommands(commands) + + // Validation functions - delegated to handler + fun validatePlateauWidth() = validationHandler.validatePlateauWidth() + + fun validatePlateauHeight() = validationHandler.validatePlateauHeight() + + fun validateRoverStartX() = validationHandler.validateRoverStartX() + + fun validateRoverStartY() = validationHandler.validateRoverStartY() + + fun validateMovementCommands() = validationHandler.validateMovementCommands() + } + +/** + * Handles field updates for the New Mission screen. + * Manages form field state updates and clears related error messages. + */ +private class FieldUpdateHandler( + private val updateState: (NewMissionUiState) -> Unit, + private val getCurrentState: () -> NewMissionUiState, +) { + fun updateJsonInput(json: String) { + updateState( + getCurrentState().copy( + jsonInput = json, + jsonError = null, + errorMessage = null ) - } + ) + } - /** - * Executes mission using builder input mode (network API execution). - */ - private suspend fun executeBuilderMission() { - val state = _uiState.value - - executeNetworkMissionUseCase - .executeFromBuilderInputs( - plateauWidth = state.plateauWidth.toIntOrNull() ?: 0, - plateauHeight = state.plateauHeight.toIntOrNull() ?: 0, - roverStartX = state.roverStartX.toIntOrNull() ?: 0, - roverStartY = state.roverStartY.toIntOrNull() ?: 0, - roverDirection = state.roverStartDirection, - movements = state.movementCommands - ).onEach { networkResult -> - handleNetworkResult(networkResult) - }.launchIn(viewModelScope) + fun updatePlateauWidth(width: String) { + updateState( + getCurrentState().copy( + plateauWidth = width, + plateauWidthError = null, + errorMessage = null + ) + ) + } + + fun updatePlateauHeight(height: String) { + updateState( + getCurrentState().copy( + plateauHeight = height, + plateauHeightError = null, + errorMessage = null + ) + ) + } + + fun updateRoverStartX(x: String) { + updateState( + getCurrentState().copy( + roverStartX = x, + roverStartXError = null, + errorMessage = null + ) + ) + } + + fun updateRoverStartY(y: String) { + updateState( + getCurrentState().copy( + roverStartY = y, + roverStartYError = null, + errorMessage = null + ) + ) + } + + fun updateRoverStartDirection(direction: String) { + updateState( + getCurrentState().copy( + roverStartDirection = direction, + roverStartDirectionError = null, + errorMessage = null + ) + ) + } + + fun updateMovementCommands(commands: String) { + updateState( + getCurrentState().copy( + movementCommands = commands, + movementCommandsError = null, + errorMessage = null + ) + ) + } +} + +/** + * Handles field validation for the New Mission screen. + */ +private class ValidationHandler( + private val updateState: (NewMissionUiState) -> Unit, + private val getCurrentState: () -> NewMissionUiState, +) { + fun validatePlateauWidth() { + val width = getCurrentState().plateauWidth + if (width.isNotBlank()) { + val widthValue = width.toIntOrNull() + if (widthValue == null || widthValue <= 0) { + updateState( + getCurrentState().copy( + plateauWidthError = "Must be a positive number" + ) + ) + } } + } - /** - * Handles network result from builder mission execution. - */ - private fun handleNetworkResult(networkResult: NetworkResult) { - when (networkResult) { - is NetworkResult.Success -> { - setSuccessState("Network mission completed! Final position: ${networkResult.data}") - } - is NetworkResult.Error -> { - setErrorState("Network error: ${networkResult.message}") - } - is NetworkResult.Loading -> { - setLoadingState() - } + fun validatePlateauHeight() { + val height = getCurrentState().plateauHeight + if (height.isNotBlank()) { + val heightValue = height.toIntOrNull() + if (heightValue == null || heightValue <= 0) { + updateState( + getCurrentState().copy( + plateauHeightError = "Must be a positive number" + ) + ) } } + } - /** - * Sets the UI to loading state. - */ - private fun setLoadingState() { - _uiState.value = - _uiState.value.copy( - isLoading = true, - errorMessage = null, - successMessage = null + fun validateRoverStartX() { + val x = getCurrentState().roverStartX + if (x.isNotBlank()) { + val xValue = x.toIntOrNull() + if (xValue == null || xValue < 0) { + updateState( + getCurrentState().copy( + roverStartXError = "Must be a non-negative number" + ) ) + } } + } - /** - * Sets the UI to success state with message. - */ - private fun setSuccessState(message: String) { - _uiState.value = - _uiState.value.copy( - isLoading = false, - successMessage = message, - errorMessage = null + fun validateRoverStartY() { + val y = getCurrentState().roverStartY + if (y.isNotBlank()) { + val yValue = y.toIntOrNull() + if (yValue == null || yValue < 0) { + updateState( + getCurrentState().copy( + roverStartYError = "Must be a non-negative number" + ) ) + } } + } - /** - * Sets the UI to error state with message. - */ - private fun setErrorState(message: String) { - _uiState.value = - _uiState.value.copy( - isLoading = false, - errorMessage = message, - successMessage = null + fun validateMovementCommands() { + val commands = getCurrentState().movementCommands + if (commands.isNotBlank()) { + if (commands.any { it !in Constants.Validation.VALID_MOVEMENT_CHARS }) { + updateState( + getCurrentState().copy( + movementCommandsError = "Must contain only L, R, M characters" + ) ) + } } + } +} - /** - * Maps RoverError to user-friendly error message. - */ - private fun mapRoverErrorToMessage(error: Throwable): String = - when (error) { - is RoverError.InvalidInputFormat -> - "Invalid JSON format: ${error.details}" +/** + * Handles state updates for the New Mission screen. + * Manages loading, success, error, and mode switching states. + */ +private class StateManager( + private val updateState: (NewMissionUiState) -> Unit, + private val getCurrentState: () -> NewMissionUiState, +) { + fun switchInputMode(mode: InputMode) { + updateState( + getCurrentState().copy( + inputMode = mode, + errorMessage = null, + successMessage = null, + jsonError = null + ) + ) + } - is RoverError.InvalidInitialPosition -> - "Rover initial position (${error.x}, ${error.y}) is outside plateau bounds " + - "(0,0) to (${error.plateauMaxX}, ${error.plateauMaxY})" + fun setLoadingState() { + updateState( + getCurrentState().copy( + isLoading = true, + errorMessage = null, + successMessage = null + ) + ) + } - is RoverError.InvalidDirectionChar -> - "Invalid direction '${error.char}'. Must be N, E, S, or W" + fun setSuccessState(message: String) { + updateState( + getCurrentState().copy( + isLoading = false, + successMessage = message, + errorMessage = null + ) + ) + } - is RoverError.InvalidPlateauDimensions -> - "Invalid plateau dimensions (${error.x}, ${error.y}). Both must be non-negative and within limits" + fun setErrorState(message: String) { + updateState( + getCurrentState().copy( + isLoading = false, + errorMessage = message, + successMessage = null + ) + ) + } - else -> - "Mission execution failed: ${error.message}" - } + fun clearMessages() { + updateState( + getCurrentState().copy( + errorMessage = null, + successMessage = null, + jsonError = null + ) + ) + } +} - /** - * Validates plateau width on focus out - */ - fun validatePlateauWidth() { - val width = _uiState.value.plateauWidth - if (width.isNotBlank()) { - val widthValue = width.toIntOrNull() - if (widthValue == null || widthValue <= 0) { - _uiState.value = - _uiState.value.copy( - plateauWidthError = "Must be a positive number" - ) - } +/** + * Handles mission execution logic. + * Coordinates between JSON and Builder mode execution with proper error handling. + */ +private class MissionExecutor( + private val executeRoverMissionUseCase: ExecuteRoverMissionUseCase, + private val executeNetworkMissionUseCase: ExecuteNetworkMissionUseCase, + private val stateManager: StateManager, + private val getCurrentState: () -> NewMissionUiState, + private val scope: CoroutineScope, +) { + suspend fun executeMission() { + stateManager.setLoadingState() + + try { + when (getCurrentState().inputMode) { + InputMode.JSON -> executeJsonMission() + InputMode.BUILDER -> executeBuilderMission() } + } catch (e: JsonParsingException) { + stateManager.setErrorState("JSON parsing error: ${e.message}") + } catch (e: MissionExecutionException) { + stateManager.setErrorState("Mission execution error: ${e.message}") + } catch (e: NetworkException) { + stateManager.setErrorState("Network error: ${e.message}") + } catch (e: NumberFormatException) { + stateManager.setErrorState("Invalid number format in input fields") + } catch (e: IllegalArgumentException) { + stateManager.setErrorState("Invalid input parameters: ${e.message}") } + } - /** - * Validates plateau height on focus out - */ - fun validatePlateauHeight() { - val height = _uiState.value.plateauHeight - if (height.isNotBlank()) { - val heightValue = height.toIntOrNull() - if (heightValue == null || heightValue <= 0) { - _uiState.value = - _uiState.value.copy( - plateauHeightError = "Must be a positive number" - ) - } + /** + * Executes mission using JSON input mode (local execution). + */ + private suspend fun executeJsonMission() { + val result = executeRoverMissionUseCase(getCurrentState().jsonInput) + + result.fold( + onSuccess = { finalPosition -> + stateManager.setSuccessState("Mission completed! Final position: $finalPosition") + }, + onFailure = { error -> + val errorMessage = mapRoverErrorToMessage(error) + stateManager.setErrorState(errorMessage) } - } + ) + } - /** - * Validates rover start X position on focus out - */ - fun validateRoverStartX() { - val x = _uiState.value.roverStartX - if (x.isNotBlank()) { - val xValue = x.toIntOrNull() - if (xValue == null || xValue < 0) { - _uiState.value = - _uiState.value.copy( - roverStartXError = "Must be a non-negative number" - ) - } + /** + * Executes mission using builder input mode (network API simulation). + */ + private suspend fun executeBuilderMission() { + val state = getCurrentState() + + executeNetworkMissionUseCase + .executeFromBuilderInputs( + plateauWidth = state.plateauWidth.toIntOrNull() ?: 0, + plateauHeight = state.plateauHeight.toIntOrNull() ?: 0, + roverStartX = state.roverStartX.toIntOrNull() ?: 0, + roverStartY = state.roverStartY.toIntOrNull() ?: 0, + roverDirection = state.roverStartDirection, + movements = state.movementCommands + ).onEach { networkResult -> + handleNetworkResult(networkResult) + }.launchIn(scope) + } + + private fun handleNetworkResult(networkResult: NetworkResult) { + when (networkResult) { + is NetworkResult.Success -> { + stateManager.setSuccessState("Network mission completed! Final position: ${networkResult.data}") } - } - /** - * Validates rover start Y position on focus out - */ - fun validateRoverStartY() { - val y = _uiState.value.roverStartY - if (y.isNotBlank()) { - val yValue = y.toIntOrNull() - if (yValue == null || yValue < 0) { - _uiState.value = - _uiState.value.copy( - roverStartYError = "Must be a non-negative number" - ) - } + is NetworkResult.Error -> { + stateManager.setErrorState("Network error: ${networkResult.message}") } - } - /** - * Validates movement commands on focus out - */ - fun validateMovementCommands() { - val commands = _uiState.value.movementCommands - if (commands.isNotBlank()) { - if (commands.any { it !in Constants.Validation.VALID_MOVEMENT_CHARS }) { - _uiState.value = - _uiState.value.copy( - movementCommandsError = "Must contain only L, R, M characters" - ) - } + is NetworkResult.Loading -> { + stateManager.setLoadingState() } } + } - /** - * Clears all success and error messages - */ - fun clearMessages() { - _uiState.value = - _uiState.value.copy( - errorMessage = null, - successMessage = null, - jsonError = null - ) + /** + * Maps RoverError to user-friendly error message. + */ + private fun mapRoverErrorToMessage(error: Throwable): String = + when (error) { + is RoverError.InvalidInputFormat -> + "Invalid JSON format: ${error.details}" + + is RoverError.InvalidInitialPosition -> + "Rover initial position (${error.x}, ${error.y}) is outside plateau bounds " + + "(0,0) to (${error.plateauMaxX}, ${error.plateauMaxY})" + + is RoverError.InvalidDirectionChar -> + "Invalid direction '${error.char}'. Must be N, E, S, or W" + + is RoverError.InvalidPlateauDimensions -> + "Invalid plateau dimensions (${error.x}, ${error.y}). Both must be non-negative and within limits" + + else -> + "Mission execution failed: ${error.message}" } - } +}