use serde::Serialize;

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ProfileSummary {
    /// The network depth.
    network_depth: usize,
    /// The number of gates.
    total_gates: usize,
    /// The network size in bytes.
    pub(crate) network_size: usize,
    /// How long does it take to do the preprocess in µs, roughly.
    pub(crate) preprocess_weight: usize,
}

/// How more expensive a µs of online time is compared to preprocess time.
const ONLINE_WEIGHT: usize = 64;
/// How long a round of network communication take.
// Clusters should try to stay close to one another. 32ms is reasonable.
const NETWORK_ROUND_TIME: usize = 1 << 15;
/// How long in µs does it take to upload a byte.
// 10 MB/s with 40 peers mean each byte takes 4µs to be sent to the whole cluster.
const NETWORK_BYTE_DURATION: usize = 4;
/// How long a local gate takes in µs.
const GATE_TIME: usize = 4;

impl ProfileSummary {
    pub fn weight(&self) -> usize {
        ONLINE_WEIGHT
            * (self.network_depth * NETWORK_ROUND_TIME
                + self.total_gates * GATE_TIME
                + self.network_size * NETWORK_BYTE_DURATION)
            + self.preprocess_weight
    }
    /// The weight function in Javascript.
    pub fn weight_js() -> String {
        let network_depth_weight = ProfileSummary {
            network_depth: 1,
            ..Default::default()
        }
        .weight();
        let gate_weight = ProfileSummary {
            total_gates: 1,
            ..Default::default()
        }
        .weight();
        let network_size_weight = ProfileSummary {
            network_size: 1,
            ..Default::default()
        }
        .weight();
        format!(
            "function weight(x){{ return {} * x.network_depth + {} * x.total_gates + {} * x.network_size + x.preprocess_weight;}}",
            network_depth_weight,
            gate_weight,
            network_size_weight)
    }
    pub fn new(
        network_depth: usize,
        total_gates: usize,
        network_size: usize,
        preprocess_weight: usize,
    ) -> Self {
        Self {
            network_depth,
            total_gates,
            network_size,
            preprocess_weight,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use boa_engine::{Context, Source};
    #[test]
    fn test_weight_javascript() {
        fn test_weight_js(profile_summary: ProfileSummary) {
            let weight = profile_summary.weight();
            let js_code = format!(
                "{}\nweight({})",
                ProfileSummary::weight_js(),
                serde_json::to_string(&profile_summary).expect("Serialization failed.")
            );
            let mut context = Context::default();
            let result = context
                .eval(Source::from_bytes(&js_code))
                .expect("Eval failed.");

            assert_eq!(result.as_number().unwrap() as usize, weight);
        }
        test_weight_js(ProfileSummary::new(5, 0, 0, 0));
        test_weight_js(ProfileSummary::new(0, 9, 0, 0));
        test_weight_js(ProfileSummary::new(0, 0, 13, 0));
        test_weight_js(ProfileSummary::new(0, 0, 0, 17));
        test_weight_js(ProfileSummary::new(1, 2, 3, 4));
        test_weight_js(ProfileSummary::new(4, 3, 2, 1));
    }
}
