rmpca 0.2.0

Enterprise-grade unified CLI for rmp.ca operations - Rust port
//! Compile-map command: Pre-process GeoJSON and serialize graph
//!
//! This command transforms raw GeoJSON into a serialized binary format (.rmp)
//! that can be loaded in milliseconds instead of parsing GeoJSON and
//! building the graph from scratch on every optimization run.

use crate::config::Config;
use crate::optimizer::RouteOptimizer;
use anyhow::{Context, Result};
use clap::Args as ClapArgs;
use std::fs::File;
use std::io::BufWriter;
use std::path::PathBuf;

#[derive(Debug, ClapArgs)]
pub struct Args {
    /// Input GeoJSON file (FeatureCollection)
    input: PathBuf,

    /// Output .rmp binary file (default: input.rmp)
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Use zero-copy rkyv serialization (default: true)
    #[arg(long, default_value = "true")]
    zero_copy: bool,

    /// Compute statistics about the graph
    #[arg(long)]
    stats: bool,
}

/// Compile-map: Pre-process GeoJSON and serialize graph for instant loading
///
/// This command transforms raw GeoJSON into a serialized binary format (.rmp)
/// that can be loaded in milliseconds instead of parsing GeoJSON and
/// building the graph from scratch on every optimization run.
///
/// Benefits:
/// - Subsequent optimize runs: 1000x faster (ms vs seconds)
/// - Reduced CPU usage for repeated optimizations on same map
/// - Consistent graph structure across optimizations
pub async fn run(args: Args) -> Result<()> {
    let config = Config::load().unwrap_or_default();
    config.init_logging();

    tracing::info!("Compiling map: {}", args.input.display());

    // Read and parse GeoJSON
    let geojson_str = std::fs::read_to_string(&args.input)
        .context("Failed to read GeoJSON file")?;

    let feature_collection: geojson::FeatureCollection = geojson_str.parse()
        .context("Failed to parse GeoJSON")?;

    tracing::info!("Loaded {} features", feature_collection.features.len());

    // Build graph from GeoJSON features
    let mut optimizer = RouteOptimizer::new();
    optimizer.build_graph_from_features(&feature_collection.features)?;

    // Serialize graph
    let output_path = args.output.unwrap_or_else(|| {
        let mut path = args.input.clone();
        path.set_extension("rmp");
        path
    });

    tracing::info!("Serializing graph to: {}", output_path.display());

    if args.zero_copy {
        // Use rkyv for zero-copy deserialization
        serialize_rkyv(&optimizer, &output_path)?;
    } else {
        // Use bincode as fallback
        serialize_bincode(&optimizer, &output_path)?;
    }

    // Print statistics if requested
    if args.stats {
        print_graph_stats(&optimizer);
    }

    tracing::info!("Map compilation complete: {}", output_path.display());
    Ok(())
}

fn serialize_rkyv(optimizer: &RouteOptimizer, path: &PathBuf) -> Result<()> {
    use crate::optimizer::OptimizerData;

    let data: OptimizerData = optimizer.into();
    let bytes = rkyv::to_bytes::<_, 4096>(&data)
        .map_err(|e| anyhow::anyhow!("Failed to serialize with rkyv: {}", e))?;

    std::fs::write(path, &bytes)
        .context("Failed to write rkyv binary file")?;

    tracing::info!("Serialized {} bytes with rkyv (zero-copy)", bytes.len());
    Ok(())
}

fn serialize_bincode(optimizer: &RouteOptimizer, path: &PathBuf) -> Result<()> {
    let serialized = bincode::serialize(optimizer)
        .context("Failed to serialize graph with bincode")?;

    std::fs::write(path, serialized)
        .context("Failed to write binary file")?;

    Ok(())
}

fn print_graph_stats(optimizer: &RouteOptimizer) {
    let stats = optimizer.get_stats();
    println!("Graph Statistics:");
    println!("  Nodes: {}", stats.node_count);
    println!("  Edges: {}", stats.edge_count);
    println!("  Components: {}", stats.component_count);
    println!("  Avg degree: {:.2}", stats.avg_degree);
    println!("  Max degree: {}", stats.max_degree);
}

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

    #[test]
    fn test_compile_map_args() {
        let args = Args {
            input: PathBuf::from("test.geojson"),
            output: None,
            zero_copy: true,
            stats: false,
        };
        assert_eq!(args.input, PathBuf::from("test.geojson"));
        assert!(args.zero_copy);
        assert!(!args.stats);
    }

    #[test]
    fn test_rkyv_roundtrip() {
        use crate::optimizer::OptimizerData;
        use rkyv::Deserialize;

        let mut optimizer = RouteOptimizer::new();
        optimizer.nodes.push(crate::optimizer::Node::new("n1", 45.5, -73.6));
        optimizer.nodes.push(crate::optimizer::Node::new("n2", 45.6, -73.7));
        optimizer.ways.push(crate::optimizer::Way::new("w1", vec!["n1".into(), "n2".into()]));

        // Serialize with rkyv
        let data: OptimizerData = (&optimizer).into();
        let bytes = rkyv::to_bytes::<_, 256>(&data).unwrap();

        // Deserialize
        let archived = unsafe { rkyv::archived_root::<OptimizerData>(&bytes) };
        let restored: OptimizerData = archived.deserialize(&mut rkyv::Infallible).unwrap();

        assert_eq!(restored.nodes.len(), 2);
        assert_eq!(restored.ways.len(), 1);
        assert_eq!(restored.nodes[0].id, "n1");
        assert_eq!(restored.nodes[1].lat, 45.6);
    }
}