bckplayer 0.1.0

A cross-platform CLI media player interface for VLC
Documentation
use anyhow::Result;
use serde::{Serialize, de::DeserializeOwned};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};

/// Trait for configuration modules that can be managed by the ConfigManager
pub trait ConfigModule: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {
    /// Unique identifier for this config module
    fn module_name() -> &'static str;
    
    /// Default configuration for this module
    fn default() -> Self;
    
    /// Validate the configuration (optional override)
    fn validate(&self) -> Result<()> {
        Ok(())
    }
    
    /// Called when config is loaded (optional override for migration/updates)
    fn on_load(&mut self) -> Result<()> {
        Ok(())
    }
    
    /// Called before config is saved (optional override for cleanup)
    fn on_save(&mut self) -> Result<()> {
        Ok(())
    }
}

/// Configuration manager that handles multiple config modules
#[derive(Clone)]
pub struct ConfigManager {
    config_path: PathBuf,
    configs: Arc<RwLock<HashMap<String, serde_json::Value>>>,
}

impl ConfigManager {
    /// Create a new config manager
    pub fn new() -> Result<Self> {
        let home_dir = dirs::home_dir()
            .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
        
        let config_dir = home_dir.join(".bckplayer");
        let config_path = config_dir.join("config.json");

        // Create config directory if it doesn't exist
        if !config_dir.exists() {
            std::fs::create_dir_all(&config_dir)?;
        }

        let manager = Self {
            config_path,
            configs: Arc::new(RwLock::new(HashMap::new())),
        };

        // Load existing config
        manager.load_all()?;

        Ok(manager)
    }

    /// Load all configurations from file
    pub fn load_all(&self) -> Result<()> {
        if !self.config_path.exists() {
            // Initialize with default configs for all known modules
            self.initialize_defaults()?;
            // Create config file with defaults
            self.save_all()?;
            return Ok(());
        }

        let content = std::fs::read_to_string(&self.config_path)?;
        let configs: HashMap<String, serde_json::Value> = if content.trim().is_empty() {
            // If file is empty, initialize defaults
            self.initialize_defaults()?;
            self.save_all()?;
            return Ok(());
        } else {
            serde_json::from_str(&content)?
        };

        if let Ok(mut config_lock) = self.configs.write() {
            *config_lock = configs;
        }

        Ok(())
    }

    /// Save all configurations to file
    pub fn save_all(&self) -> Result<()> {
        let configs = self.configs.read()
            .map_err(|_| anyhow::anyhow!("Failed to read configs for saving"))?;

        let content = serde_json::to_string_pretty(&*configs)?;
        std::fs::write(&self.config_path, content)?;

        Ok(())
    }

    /// Get a configuration module
    pub fn get<T: ConfigModule>(&self) -> T {
        let module_name = T::module_name();
        
        let configs = match self.configs.read() {
            Ok(configs) => configs,
            Err(_) => return T::default(),
        };

        match configs.get(module_name) {
            Some(value) => {
                match serde_json::from_value::<T>(value.clone()) {
                    Ok(mut config) => {
                        // Call on_load hook
                        let _ = config.on_load();
                        config
                    }
                    Err(_) => T::default(),
                }
            }
            None => T::default(),
        }
    }

    /// Set a configuration module
    pub fn set<T: ConfigModule>(&self, mut config: T) -> Result<()> {
        let module_name = T::module_name();

        // Validate the config
        config.validate()?;

        // Call on_save hook
        config.on_save()?;

        // Serialize to JSON value
        let value = serde_json::to_value(&config)?;

        // Update configs
        {
            let mut configs = self.configs.write()
                .map_err(|_| anyhow::anyhow!("Failed to write configs"))?;
            configs.insert(module_name.to_string(), value);
        }

        // Force immediate save to ensure persistence
        self.save_all()?;

        Ok(())
    }

    /// Update a configuration module using a closure
    pub fn update<T: ConfigModule, F>(&self, updater: F) -> Result<()>
    where
        F: FnOnce(&mut T),
    {
        let mut config = self.get::<T>();
        updater(&mut config);
        self.set(config)
    }

    /// Check if a config module exists
    pub fn has_module<T: ConfigModule>(&self) -> bool {
        let module_name = T::module_name();
        let configs = match self.configs.read() {
            Ok(configs) => configs,
            Err(_) => return false,
        };
        configs.contains_key(module_name)
    }

    /// Remove a config module
    pub fn remove_module<T: ConfigModule>(&self) -> Result<()> {
        let module_name = T::module_name();
        
        {
            let mut configs = self.configs.write()
                .map_err(|_| anyhow::anyhow!("Failed to write configs"))?;
            configs.remove(module_name);
        }

        // Save immediately after removing
        self.save_all()?;
        Ok(())
    }

    /// Get all module names
    pub fn get_module_names(&self) -> Vec<String> {
        let configs = match self.configs.read() {
            Ok(configs) => configs,
            Err(_) => return Vec::new(),
        };
        configs.keys().cloned().collect()
    }

    /// Force save now
    pub fn sync(&self) -> Result<()> {
        self.save_all()
    }

    /// Get config file path
    pub fn get_config_path(&self) -> &PathBuf {
        &self.config_path
    }

    /// Get config file size
    pub fn get_config_size(&self) -> Result<u64> {
        Ok(std::fs::metadata(&self.config_path)?.len())
    }

    /// Export config to a different file
    pub fn export_to(&self, path: &PathBuf) -> Result<()> {
        let configs = self.configs.read()
            .map_err(|_| anyhow::anyhow!("Failed to read configs for export"))?;
        
        let content = serde_json::to_string_pretty(&*configs)?;
        std::fs::write(path, content)?;
        Ok(())
    }

    /// Import config from a file
    pub fn import_from(&self, path: &PathBuf) -> Result<()> {
        let content = std::fs::read_to_string(path)?;
        let configs: HashMap<String, serde_json::Value> = serde_json::from_str(&content)?;

        {
            let mut config_lock = self.configs.write()
                .map_err(|_| anyhow::anyhow!("Failed to write configs during import"))?;
            *config_lock = configs;
        }

        self.save_all()?;
        Ok(())
    }

    /// Reset all configs to defaults
    pub fn reset_all(&self) -> Result<()> {
        {
            let mut configs = self.configs.write()
                .map_err(|_| anyhow::anyhow!("Failed to write configs during reset"))?;
            configs.clear();
        }
        self.save_all()?;
        Ok(())
    }

    /// Initialize all known config modules with their default values
    fn initialize_defaults(&self) -> Result<()> {
        use crate::config::Config;
        use crate::config_modules::{YoutubeConfig, NetworkConfig, AppearanceConfig, MediaConfig, AdvancedConfig};

        // Initialize all known config modules with defaults
        let mut configs = self.configs.write()
            .map_err(|_| anyhow::anyhow!("Failed to write configs during initialization"))?;

        // Set default values for each module using ConfigModule trait
        let player_config = <Config as ConfigModule>::default();
        let youtube_config = <YoutubeConfig as ConfigModule>::default();
        let network_config = <NetworkConfig as ConfigModule>::default();
        let appearance_config = <AppearanceConfig as ConfigModule>::default();
        let media_config = <MediaConfig as ConfigModule>::default();
        let advanced_config = <AdvancedConfig as ConfigModule>::default();

        // Serialize and store each config
        configs.insert("player".to_string(), serde_json::to_value(player_config)?);
        configs.insert("youtube".to_string(), serde_json::to_value(youtube_config)?);
        configs.insert("network".to_string(), serde_json::to_value(network_config)?);
        configs.insert("appearance".to_string(), serde_json::to_value(appearance_config)?);
        configs.insert("media".to_string(), serde_json::to_value(media_config)?);
        configs.insert("advanced".to_string(), serde_json::to_value(advanced_config)?);

        Ok(())
    }
}

impl Default for ConfigManager {
    fn default() -> Self {
        Self::new().expect("Failed to create default ConfigManager")
    }
}