crabcamera 0.8.3

Advanced cross-platform camera integration for Tauri applications
Documentation
use crate::commands::capture::capture_single_photo;
use crate::quality::{BlurDetector, BlurMetrics, ExposureAnalyzer, ExposureMetrics};
use crate::quality::{QualityReport, QualityValidator, ValidationConfig};
use crate::types::CameraFrame;
use std::sync::Arc;
use tauri::command;
use tokio::sync::RwLock;

// Global quality validator
lazy_static::lazy_static! {
    static ref QUALITY_VALIDATOR: Arc<RwLock<QualityValidator>> = Arc::new(RwLock::new(QualityValidator::default()));
}

/// Validate quality of a captured frame
#[command]
pub async fn validate_frame_quality(
    device_id: Option<String>,
    capture_format: Option<crate::types::CameraFormat>,
) -> Result<QualityReport, String> {
    log::info!("Validating frame quality for device: {:?}", device_id);

    // Capture a frame first
    let frame = capture_single_photo(device_id, capture_format).await?;

    // Validate quality
    let validator = QUALITY_VALIDATOR.read().await;
    let report = validator.validate_frame(&frame);

    Ok(report)
}

/// Validate quality of provided frame data
#[command]
pub async fn validate_provided_frame(frame: CameraFrame) -> Result<QualityReport, String> {
    log::info!(
        "Validating provided frame: {}x{}",
        frame.width,
        frame.height
    );

    let validator = QUALITY_VALIDATOR.read().await;
    let report = validator.validate_frame(&frame);

    Ok(report)
}

/// Analyze blur in a captured frame
#[command]
pub async fn analyze_frame_blur(
    device_id: Option<String>,
    capture_format: Option<crate::types::CameraFormat>,
) -> Result<BlurMetrics, String> {
    log::info!("Analyzing frame blur for device: {:?}", device_id);

    // Capture a frame
    let frame = capture_single_photo(device_id, capture_format).await?;

    // Analyze blur
    let blur_detector = BlurDetector::default();
    let metrics = blur_detector.analyze_frame(&frame);

    Ok(metrics)
}

/// Analyze exposure in a captured frame
#[command]
pub async fn analyze_frame_exposure(
    device_id: Option<String>,
    capture_format: Option<crate::types::CameraFormat>,
) -> Result<ExposureMetrics, String> {
    log::info!("Analyzing frame exposure for device: {:?}", device_id);

    // Capture a frame
    let frame = capture_single_photo(device_id, capture_format).await?;

    // Analyze exposure
    let exposure_analyzer = ExposureAnalyzer::default();
    let metrics = exposure_analyzer.analyze_frame(&frame);

    Ok(metrics)
}

/// Update quality validation configuration
#[command]
pub async fn update_quality_config(config: ValidationConfigDto) -> Result<String, String> {
    log::info!("Updating quality validation configuration");

    let validation_config = ValidationConfig {
        blur_threshold: config.blur_threshold,
        exposure_threshold: config.exposure_threshold,
        overall_threshold: config.overall_threshold,
        min_resolution: (config.min_width, config.min_height),
        max_noise_level: config.max_noise_level,
    };

    let validator = QualityValidator::new(validation_config);
    let mut guard = QUALITY_VALIDATOR.write().await;
    *guard = validator;

    Ok("Quality validation configuration updated".to_string())
}

/// Get current quality validation configuration
#[command]
pub async fn get_quality_config() -> Result<ValidationConfigDto, String> {
    let validator = QUALITY_VALIDATOR.read().await;
    let config = validator.config();

    Ok(ValidationConfigDto {
        blur_threshold: config.blur_threshold,
        exposure_threshold: config.exposure_threshold,
        overall_threshold: config.overall_threshold,
        min_width: config.min_resolution.0,
        min_height: config.min_resolution.1,
        max_noise_level: config.max_noise_level,
    })
}

/// Capture and validate multiple frames, return best quality
#[command]
pub async fn capture_best_quality_frame(
    device_id: Option<String>,
    capture_format: Option<crate::types::CameraFormat>,
    num_attempts: Option<u32>,
) -> Result<CaptureQualityResult, String> {
    let attempts = num_attempts.unwrap_or(5).min(10); // Max 10 attempts
    log::info!("Capturing best quality frame with {} attempts", attempts);

    let validator = QUALITY_VALIDATOR.read().await;
    let mut best_frame: Option<CameraFrame> = None;
    let mut best_report: Option<QualityReport> = None;
    let mut best_score = 0.0f32;

    for attempt in 1..=attempts {
        log::debug!("Quality capture attempt {} of {}", attempt, attempts);

        // Capture frame
        match capture_single_photo(device_id.clone(), capture_format.clone()).await {
            Ok(frame) => {
                // Validate quality
                let report = validator.validate_frame(&frame);

                if report.score.overall > best_score {
                    best_score = report.score.overall;
                    best_frame = Some(frame);
                    best_report = Some(report);
                }

                // If we achieve excellent quality, stop early
                if best_score >= 0.9 {
                    log::info!("Excellent quality achieved on attempt {}", attempt);
                    break;
                }
            }
            Err(e) => {
                log::warn!("Frame capture failed on attempt {}: {}", attempt, e);
                continue;
            }
        }

        // Small delay between attempts
        if attempt < attempts {
            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        }
    }

    match (best_frame, best_report) {
        (Some(frame), Some(report)) => Ok(CaptureQualityResult {
            frame,
            quality_report: report,
            attempts_used: attempts,
        }),
        _ => Err("Failed to capture any valid frames".to_string()),
    }
}

/// Auto-capture with quality threshold
#[command]
pub async fn auto_capture_with_quality(
    device_id: Option<String>,
    capture_format: Option<crate::types::CameraFormat>,
    min_quality_threshold: Option<f32>,
    max_attempts: Option<u32>,
    timeout_seconds: Option<u32>,
) -> Result<CaptureQualityResult, String> {
    let quality_threshold = min_quality_threshold.unwrap_or(0.7);
    let max_tries = max_attempts.unwrap_or(20).min(50); // Max 50 attempts
    let timeout = timeout_seconds.unwrap_or(30); // 30 second timeout

    log::info!(
        "Auto-capturing with quality threshold {} (max {} attempts, {}s timeout)",
        quality_threshold,
        max_tries,
        timeout
    );

    let start_time = std::time::Instant::now();
    let validator = QUALITY_VALIDATOR.read().await;

    for attempt in 1..=max_tries {
        // Check timeout
        if start_time.elapsed().as_secs() >= timeout as u64 {
            return Err(format!("Auto-capture timeout after {} seconds", timeout));
        }

        log::debug!("Auto-capture attempt {} of {}", attempt, max_tries);

        // Capture frame
        match capture_single_photo(device_id.clone(), capture_format.clone()).await {
            Ok(frame) => {
                // Validate quality
                let report = validator.validate_frame(&frame);

                if report.score.overall >= quality_threshold {
                    log::info!(
                        "Quality threshold met on attempt {} (score: {:.3})",
                        attempt,
                        report.score.overall
                    );

                    return Ok(CaptureQualityResult {
                        frame,
                        quality_report: report,
                        attempts_used: attempt,
                    });
                }

                log::debug!(
                    "Quality not met (score: {:.3}), continuing...",
                    report.score.overall
                );
            }
            Err(e) => {
                log::warn!("Frame capture failed on attempt {}: {}", attempt, e);
            }
        }

        // Small delay between attempts
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
    }

    Err(format!(
        "Failed to capture frame meeting quality threshold {} after {} attempts",
        quality_threshold, max_tries
    ))
}

/// Analyze quality trends over multiple captures
#[command]
pub async fn analyze_quality_trends(
    device_id: Option<String>,
    capture_format: Option<crate::types::CameraFormat>,
    num_samples: Option<u32>,
) -> Result<QualityTrendAnalysis, String> {
    let samples = num_samples.unwrap_or(10).min(20); // Max 20 samples
    log::info!("Analyzing quality trends over {} samples", samples);

    let validator = QUALITY_VALIDATOR.read().await;
    let mut reports = Vec::new();

    for i in 1..=samples {
        log::debug!("Quality trend sample {} of {}", i, samples);

        match capture_single_photo(device_id.clone(), capture_format.clone()).await {
            Ok(frame) => {
                let report = validator.validate_frame(&frame);
                reports.push(report);
            }
            Err(e) => {
                log::warn!("Failed to capture sample {}: {}", i, e);
                continue;
            }
        }

        // Small delay between samples
        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
    }

    if reports.is_empty() {
        return Err("No valid samples captured for trend analysis".to_string());
    }

    // Calculate trend statistics
    let scores: Vec<f32> = reports.iter().map(|r| r.score.overall).collect();
    let blur_scores: Vec<f32> = reports.iter().map(|r| r.score.blur).collect();
    let exposure_scores: Vec<f32> = reports.iter().map(|r| r.score.exposure).collect();

    let avg_quality = scores.iter().sum::<f32>() / scores.len() as f32;
    let avg_blur = blur_scores.iter().sum::<f32>() / blur_scores.len() as f32;
    let avg_exposure = exposure_scores.iter().sum::<f32>() / exposure_scores.len() as f32;

    let quality_variance = scores
        .iter()
        .map(|&x| (x - avg_quality).powi(2))
        .sum::<f32>()
        / scores.len() as f32;

    let stability_score = (1.0 - quality_variance.sqrt()).clamp(0.0, 1.0);

    Ok(QualityTrendAnalysis {
        samples_analyzed: reports.len() as u32,
        average_quality: avg_quality,
        average_blur_score: avg_blur,
        average_exposure_score: avg_exposure,
        quality_variance,
        stability_score,
        best_score: scores.iter().fold(0.0f32, |a, &b| a.max(b)),
        worst_score: scores.iter().fold(1.0f32, |a, &b| a.min(b)),
        acceptable_ratio: reports.iter().filter(|r| r.is_acceptable).count() as f32
            / reports.len() as f32,
    })
}

// Data transfer objects for Tauri commands

/// Validation configuration DTO
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ValidationConfigDto {
    pub blur_threshold: f32,
    pub exposure_threshold: f32,
    pub overall_threshold: f32,
    pub min_width: u32,
    pub min_height: u32,
    pub max_noise_level: f32,
}

/// Capture with quality result
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CaptureQualityResult {
    pub frame: CameraFrame,
    pub quality_report: QualityReport,
    pub attempts_used: u32,
}

/// Quality trend analysis result
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct QualityTrendAnalysis {
    pub samples_analyzed: u32,
    pub average_quality: f32,
    pub average_blur_score: f32,
    pub average_exposure_score: f32,
    pub quality_variance: f32,
    pub stability_score: f32,
    pub best_score: f32,
    pub worst_score: f32,
    pub acceptable_ratio: f32,
}

#[cfg(test)]
mod tests {
    use super::*;

    fn create_test_frame() -> CameraFrame {
        let data = vec![128u8; 640 * 480 * 3];
        CameraFrame::new(data, 640, 480, "test".to_string())
    }

    #[tokio::test]
    async fn test_validate_provided_frame() {
        let frame = create_test_frame();
        let result = validate_provided_frame(frame).await;
        assert!(result.is_ok());

        let report = result.unwrap();
        assert!(report.score.overall >= 0.0 && report.score.overall <= 1.0);
    }

    #[tokio::test]
    async fn test_quality_config_update() {
        let config = ValidationConfigDto {
            blur_threshold: 0.8,
            exposure_threshold: 0.8,
            overall_threshold: 0.9,
            min_width: 1920,
            min_height: 1080,
            max_noise_level: 0.2,
        };

        let result = update_quality_config(config.clone()).await;
        assert!(result.is_ok());

        let retrieved_config = get_quality_config().await.unwrap();
        // Verify the config was actually stored and retrieved correctly
        assert!((retrieved_config.blur_threshold - 0.8).abs() < 0.001);
        assert!((retrieved_config.exposure_threshold - 0.8).abs() < 0.001);
        assert!((retrieved_config.overall_threshold - 0.9).abs() < 0.001);
        assert_eq!(retrieved_config.min_width, 1920);
        assert_eq!(retrieved_config.min_height, 1080);
        assert!((retrieved_config.max_noise_level - 0.2).abs() < 0.001);
    }

    #[tokio::test]
    async fn test_quality_trend_analysis_empty() {
        // This test may fail in CI without camera, but validates the structure
        let result =
            analyze_quality_trends(Some("invalid_camera".to_string()), None, Some(1)).await;
        // Should handle gracefully when no camera is available (may succeed with mock data)
        // In CI, this might succeed due to mock camera system
        assert!(result.is_ok() || result.is_err());
    }
}