mecha10-sim 0.6.2

Mecha10 simulation support — scene generation, asset management
Documentation
#![allow(dead_code)]

//! Simulation service for scene generation and management
//!
//! This service provides operations for generating and managing simulations,
//! including robot scene generation, environment selection, and validation.

use crate::paths;
use crate::sim::{EnvironmentSelector, RobotGenerator, RobotProfile};
use anyhow::Result;
use std::path::{Path, PathBuf};

/// Simulation service for managing simulations
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::SimulationService;
/// use std::path::Path;
///
/// # async fn example() -> anyhow::Result<()> {
/// let service = SimulationService::new();
///
/// // Generate simulation
/// service.generate(
///     Path::new("mecha10.json"),
///     Path::new("simulation/output"),
///     5, // max environments
///     0  // min score
/// )?;
/// # Ok(())
/// # }
/// ```
pub struct SimulationService {
    /// Base path for simulation output
    base_path: PathBuf,
}

impl SimulationService {
    /// Create a new simulation service with default paths
    pub fn new() -> Self {
        Self {
            base_path: PathBuf::from(paths::project::SIMULATION_OUTPUT_DIR),
        }
    }

    /// Create a simulation service with custom base path
    ///
    /// # Arguments
    ///
    /// * `base_path` - Base directory for simulation output
    pub fn with_base_path(base_path: impl Into<PathBuf>) -> Self {
        Self {
            base_path: base_path.into(),
        }
    }

    /// Generate simulation scenes from project configuration
    ///
    /// # Arguments
    ///
    /// * `config_path` - Path to mecha10.json
    /// * `output_path` - Output directory for simulation files
    /// * `max_envs` - Maximum number of environments to generate
    /// * `min_score` - Minimum compatibility score (0-100)
    pub fn generate(
        &self,
        config_path: &Path,
        output_path: &Path,
        max_envs: usize,
        min_score: i32,
    ) -> Result<GenerationResult> {
        // Load robot profile
        let profile = RobotProfile::from_config_file(config_path)?;

        // Generate robot scene
        let robot_output = output_path.join("robot.tscn");
        let robot_generator = RobotGenerator::from_config_file(config_path)?;
        robot_generator.generate(&robot_output)?;

        // Select environments
        let selector = EnvironmentSelector::new()?;
        let matches = selector.select_environments(&profile, max_envs)?;

        let filtered_matches: Vec<_> = matches.into_iter().filter(|m| m.score >= min_score).collect();

        if filtered_matches.is_empty() {
            return Err(anyhow::anyhow!(
                "No matching environments found (min score: {})",
                min_score
            ));
        }

        // Count available environments
        let _catalog_path = PathBuf::from(paths::framework::ROBOT_TASKS_CATALOG);
        let base_path = PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
        let mut available_count = 0;

        for env_match in &filtered_matches {
            let env_path = base_path.join(&env_match.environment.path);
            if env_path.exists() {
                available_count += 1;
            }
        }

        Ok(GenerationResult {
            robot_scene: robot_output,
            environments: filtered_matches.iter().map(|m| m.environment.id.clone()).collect(),
            available_count,
        })
    }

    /// Generate only the robot scene
    ///
    /// # Arguments
    ///
    /// * `config_path` - Path to mecha10.json
    /// * `output_path` - Output path for robot.tscn
    pub fn generate_robot(&self, config_path: &Path, output_path: &Path) -> Result<()> {
        let robot_generator = RobotGenerator::from_config_file(config_path)?;
        robot_generator.generate(output_path)
    }

    /// List available environments
    ///
    /// # Arguments
    ///
    /// * `config_path` - Path to mecha10.json
    /// * `verbose` - Show detailed information
    pub fn list_environments(&self, config_path: &Path, verbose: bool) -> Result<Vec<EnvironmentInfo>> {
        let profile = RobotProfile::from_config_file(config_path)?;
        let selector = EnvironmentSelector::new()?;
        let matches = selector.select_environments(&profile, 100)?; // Get all

        let base_path = PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
        let mut environments = Vec::new();

        for env_match in matches {
            let env_path = base_path.join(&env_match.environment.path);
            let available = env_path.exists();

            if verbose || available {
                environments.push(EnvironmentInfo {
                    id: env_match.environment.id.clone(),
                    name: env_match.environment.name.clone(),
                    description: env_match.environment.description.clone(),
                    score: env_match.score,
                    available,
                });
            }
        }

        Ok(environments)
    }

    /// Validate project configuration for simulation
    ///
    /// # Arguments
    ///
    /// * `config_path` - Path to mecha10.json
    pub fn validate_config(&self, config_path: &Path) -> Result<ValidationResult> {
        let profile = RobotProfile::from_config_file(config_path)?;

        let mut errors = Vec::new();
        let mut warnings = Vec::new();

        // Validate platform
        if profile.platform.is_empty() {
            errors.push("Platform not specified".to_string());
        }

        // Validate sensors
        if profile.sensors.is_empty() {
            warnings.push("No sensors configured".to_string());
        }

        // Validate task nodes
        if profile.task_nodes.is_empty() {
            warnings.push("No task nodes configured".to_string());
        }

        let is_valid = errors.is_empty();

        Ok(ValidationResult {
            is_valid,
            errors,
            warnings,
            platform: profile.platform,
            sensor_count: profile.sensors.len(),
            node_count: profile.task_nodes.len(),
        })
    }

    /// Check if simulation is properly set up
    pub fn is_setup(&self) -> bool {
        self.base_path.exists() && self.base_path.join("robot.tscn").exists()
    }

    /// Get the base path for simulations
    pub fn base_path(&self) -> &Path {
        &self.base_path
    }

    /// Resolve a model path from package namespace or plain name
    ///
    /// Supports multiple formats:
    /// - Package namespace: "@mecha10/simulation-models/rover"
    /// - Plain name: "rover"
    ///
    /// Resolution order:
    /// 1. Framework (monorepo): packages/simulation/models/{name}/
    /// 2. Project-local: simulation/models/{name}/
    ///
    /// # Arguments
    ///
    /// * `model_ref` - Model reference (package namespace or plain name)
    ///
    /// # Returns
    ///
    /// Absolute path to the model directory, or error if not found
    pub fn resolve_model_path(&self, model_ref: &str) -> Result<PathBuf> {
        Self::resolve_asset_path(
            model_ref,
            AssetPathSpec {
                kind: "Model",
                namespace_prefix: "@mecha10/simulation-models/",
                project_dir: paths::project::SIMULATION_MODELS_DIR,
                framework_dir: paths::framework::SIMULATION_MODELS_DIR,
                bundled_subdir: "simulation/models",
                cached_path: |assets| assets.models_path(),
            },
        )
    }

    /// Resolve an environment path from package namespace or plain name
    ///
    /// Supports multiple formats:
    /// - Package namespace: "@mecha10/simulation-environments/basic_arena"
    /// - Plain name: "basic_arena"
    ///
    /// Resolution order:
    /// 1. Project-local: simulation/environments/{name}/ (project customizations take precedence)
    /// 2. Framework (monorepo): packages/simulation/environments/{name}/
    ///
    /// # Arguments
    ///
    /// * `env_ref` - Environment reference (package namespace or plain name)
    ///
    /// # Returns
    ///
    /// Absolute path to the environment directory, or error if not found
    pub fn resolve_environment_path(&self, env_ref: &str) -> Result<PathBuf> {
        Self::resolve_asset_path(
            env_ref,
            AssetPathSpec {
                kind: "Environment",
                namespace_prefix: "@mecha10/simulation-environments/",
                project_dir: paths::project::SIMULATION_ENVIRONMENTS_DIR,
                framework_dir: paths::framework::SIMULATION_ENVIRONMENTS_DIR,
                bundled_subdir: "simulation/environments",
                cached_path: |assets| assets.environments_path(),
            },
        )
    }

    /// Shared resolution logic for `resolve_model_path`/`resolve_environment_path`.
    ///
    /// Both look up an asset (model or environment) by package-namespace or plain
    /// name, checking project-local, `MECHA10_FRAMEWORK_PATH`, compile-time bundled,
    /// and cached-release locations, in that order.
    fn resolve_asset_path(asset_ref: &str, spec: AssetPathSpec) -> Result<PathBuf> {
        // Strip package namespace if present
        let asset_name = asset_ref.strip_prefix(spec.namespace_prefix).unwrap_or(asset_ref);

        // Try project-local path FIRST (project customizations take precedence)
        let project_path = PathBuf::from(spec.project_dir).join(asset_name);
        if project_path.exists() {
            return Ok(project_path);
        }

        // Try MECHA10_FRAMEWORK_PATH environment variable
        if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
            let framework_asset_path = PathBuf::from(framework_path).join(spec.framework_dir).join(asset_name);
            if framework_asset_path.exists() {
                return Ok(framework_asset_path);
            }
        }

        // Try compile-time path from CLI package (packages/cli -> packages/simulation)
        let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        if let Some(packages_dir) = cli_manifest_dir.parent() {
            let bundled_asset_path = packages_dir.join(spec.bundled_subdir).join(asset_name);
            if bundled_asset_path.exists() {
                return Ok(bundled_asset_path);
            }
        }

        // Try cached assets from GitHub releases (~/.mecha10/simulation/current/{models,environments}/)
        let assets_service = crate::services::SimulationAssetsService::new();
        if let Some(cached_dir) = (spec.cached_path)(&assets_service) {
            let cached_asset_path = cached_dir.join(asset_name);
            if cached_asset_path.exists() {
                return Ok(cached_asset_path);
            }
        }

        Err(anyhow::anyhow!(
            "{} not found: {}\n\nChecked:\n  • Project: {}/{}\n  • Framework: packages/{}/{}\n  • Cached: ~/.mecha10/simulation/current/{}/{}",
            spec.kind,
            asset_ref,
            spec.project_dir,
            asset_name,
            spec.bundled_subdir,
            asset_name,
            spec.bundled_subdir.rsplit('/').next().unwrap_or(spec.bundled_subdir),
            asset_name
        ))
    }
}

/// Parameters distinguishing a model-path resolution from an environment-path
/// resolution in [`SimulationService::resolve_asset_path`].
struct AssetPathSpec {
    kind: &'static str,
    namespace_prefix: &'static str,
    project_dir: &'static str,
    framework_dir: &'static str,
    bundled_subdir: &'static str,
    cached_path: fn(&crate::services::SimulationAssetsService) -> Option<PathBuf>,
}

impl Default for SimulationService {
    fn default() -> Self {
        Self::new()
    }
}

/// Result of simulation generation
#[derive(Debug)]
pub struct GenerationResult {
    pub robot_scene: PathBuf,
    pub environments: Vec<String>,
    pub available_count: usize,
}

/// Information about an environment
#[derive(Debug, Clone)]
pub struct EnvironmentInfo {
    pub id: String,
    pub name: String,
    pub description: String,
    pub score: i32,
    pub available: bool,
}

/// Result of configuration validation
#[derive(Debug)]
pub struct ValidationResult {
    pub is_valid: bool,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
    pub platform: String,
    pub sensor_count: usize,
    pub node_count: usize,
}