Analyze dependencies of cargo projects
use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};

use cargo_metadata::PackageId;
use serde::{Deserialize, Serialize};

// Use an alternative profile so clean-building is "sandboxed" to this tool
const CARGO_CUSTOM_PROFILE: [&str; 2] = ["--config", r#"profile.depwiz.inherits="dev""#];

/// See https://doc.rust-lang.org/nightly/nightly-rustc/cargo/util/machine_message/struct.TimingInfo.html#method.reason
/// This should always be "timing-info"
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum Reason {
    TimingInfo,
}

/// See https://docs.rs/cargo/latest/cargo/core/compiler/enum.CompileMode.html
/// These are the only two cases we are interested in
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
    Build,
    RunCustomBuild,
}

/// See https://docs.rs/cargo/latest/cargo/util/machine_message/struct.TimingInfo.html
/// The `Reason` enum only has a `timing-info` variant so this should make sure cargo isn't
/// generating any unexpected messages
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message {
    reason: Reason,
    pub package_id: PackageId,
    pub target: super::Target,
    pub mode: Mode,
    pub duration: f64,
    pub rmeta_time: Option<f64>,
}

#[derive(Clone, Debug)]
pub struct Output {
    pub repr: HashMap<PackageId, Vec<Message>>,
}

impl Output {
    pub fn generate() -> Vec<u8> {
        // TODO: `cargo build --timings=json` seems to only work on a clean build
        Command::new("cargo")
            .arg("clean")
            .args(CARGO_CUSTOM_PROFILE)
            .args(&["--profile", "depwiz"])
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .output()
            .unwrap();

        let timings_output = Command::new("cargo")
            .arg("build")
            .args(CARGO_CUSTOM_PROFILE)
            .args(&[
                "--profile",
                "depwiz",
                "-Zunstable-options",
                "--timings=json",
            ])
            .stderr(Stdio::inherit())
            .output()
            .unwrap();
        timings_output.stdout
    }

    pub fn new(source_data: &[u8]) -> Self {
        let mut timings = HashMap::new();
        let mut timings_buffer = BufReader::new(source_data);
        let mut message_buffer = Vec::new();

        loop {
            timings_buffer
                .read_until(b'\n', &mut message_buffer)
                .unwrap();

            let json_message: Message = match serde_json::from_slice(&message_buffer) {
                Ok(message) => message,
                Err(err) => {
                    use serde_json::error::Category;
                    match err.classify() {
                        Category::Eof => break,
                        Category::Data => todo!(
                            "JSON object not properly handled, got `{err:?}` for message: `{}`",
                            String::from_utf8(message_buffer.clone()).unwrap()
                        ),
                        _ => {
                            panic!("Unexpected error while parsing: {err:?}");
                        }
                    }
                }
            };

            let pkg_timings: &mut Vec<Message> =
                timings.entry(json_message.package_id.clone()).or_default();
            pkg_timings.push(json_message);

            message_buffer.clear();
        }

        Self { repr: timings }
    }
}