Analyze dependencies of cargo projects
use crate::{AnnotationGraph, Measurement, Variable};

use charming::{component::Axis, datatype::DataPoint, element::AxisType};

const BUCKET_COUNT: usize = 10;
const DECIMAL_PLACES: usize = 50;

fn find_min_max_duration(pkg_durations: &[f64]) -> (f64, f64) {
    assert!(!pkg_durations.is_empty());

    let mut min = f64::MAX;
    let mut max = f64::MIN;
    for duration in pkg_durations {
        min = duration.min(min);
        max = duration.max(max);
    }

    assert_ne!(min, f64::MAX);
    assert_ne!(max, f64::MIN);
    assert!(min.is_sign_positive());
    assert!(max.is_sign_positive());

    (min, max)
}

pub fn axes(annotations: &AnnotationGraph) -> (Axis, Axis) {
    let packages = annotations.packages();
    let pkg_durations = packages
        .map(|id| annotations.variable(id, Variable::UnitDuration, Measurement::Exact))
        .collect::<Vec<_>>();

    let (min_duration, max_duration) = find_min_max_duration(&pkg_durations);
    let bucket_width = (max_duration - min_duration) / (BUCKET_COUNT as f64);

    let mut x_labels = Vec::with_capacity(BUCKET_COUNT);
    for bucket_index in 0..BUCKET_COUNT {
        // The start time is offset, first bucket starts at min_duration
        let start_time = min_duration + (bucket_width * (bucket_index as f64));
        // The label is the start time rounded to 2 decimal places
        x_labels.push(format!("{start_time:.2}"));
    }

    let x_axis = Axis::new().type_(AxisType::Category).data(x_labels);
    let y_axis = Axis::new().type_(AxisType::Value);

    (x_axis, y_axis)
}

pub fn data(annotations: &AnnotationGraph) -> Vec<DataPoint> {
    let packages = annotations.packages();
    let pkg_durations = packages
        .map(|id| annotations.variable(id, Variable::UnitDuration, Measurement::Exact))
        .collect::<Vec<_>>();
    let (min_duration, max_duration) = find_min_max_duration(&pkg_durations);

    // Make sure to start the buckets at min, not 0
    let bucket_width = (max_duration - min_duration) / (BUCKET_COUNT as f64);
    let mut buckets = [0_u64; BUCKET_COUNT];

    for duration in pkg_durations {
        let relative_duration = duration - min_duration;
        let remainder = relative_duration % bucket_width;
        // Calculate the nearest multiple of bucket_size
        let next_multiple = (relative_duration - remainder) / bucket_width;

        // Make sure we have an integer before casting
        let float_error_margin = 2_f64.powf(1_f64 - (DECIMAL_PLACES as f64));
        assert!((next_multiple - next_multiple.round()).abs() <= float_error_margin);
        // assert_eq!(next_multiple, next_multiple.round());
        let bucket_index = (next_multiple.round()) as usize;

        // Increment the frequency of the relevant bucket
        if bucket_index == BUCKET_COUNT {
            // The max duration should be the only one in this final bucket
            assert_eq!(duration, max_duration);
            buckets[bucket_index - 1] += 1;
        } else {
            // Every other duration should hit this case instead
            buckets[bucket_index] += 1;
        }
    }

    // Convert the buckets into `charming::datatype::DataPoint`s
    buckets
        .into_iter()
        .map(|bucket| (bucket as i64).into())
        .collect()
}