use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

use console::{Style, Term};
use indicatif::{HumanDuration, ProgressBar, ProgressStyle};
use rand::Rng;

static CRATES: &[(&str, &str)] = &[
    ("console", "v0.14.1"),
    ("lazy_static", "v1.4.0"),
    ("libc", "v0.2.93"),
    ("regex", "v1.4.6"),
    ("regex-syntax", "v0.6.23"),
    ("terminal_size", "v0.1.16"),
    ("libc", "v0.2.93"),
    ("unicode-width", "v0.1.8"),
    ("lazy_static", "v1.4.0"),
    ("number_prefix", "v0.4.0"),
    ("regex", "v1.4.6"),
    ("rand", "v0.8.3"),
    ("getrandom", "v0.2.2"),
    ("cfg-if", "v1.0.0"),
    ("libc", "v0.2.93"),
    ("rand_chacha", "v0.3.0"),
    ("ppv-lite86", "v0.2.10"),
    ("rand_core", "v0.6.2"),
    ("getrandom", "v0.2.2"),
    ("rand_core", "v0.6.2"),
    ("tokio", "v1.5.0"),
    ("bytes", "v1.0.1"),
    ("pin-project-lite", "v0.2.6"),
    ("slab", "v0.4.3"),
    ("indicatif", "v0.15.0"),
];

fn main() {
    // number of cpus
    const NUM_CPUS: usize = 4;
    let start = Instant::now();

    // mimic cargo progress bar although it behaves a bit different
    let pb = ProgressBar::new(CRATES.len() as u64);
    pb.set_style(
        ProgressStyle::with_template(
            // note that bar size is fixed unlike cargo which is dynamic
            // and also the truncation in cargo uses trailers (`...`)
            if Term::stdout().size().1 > 80 {
                "{prefix:>12.cyan.bold} [{bar:57}] {pos}/{len} {wide_msg}"
            } else {
                "{prefix:>12.cyan.bold} [{bar:57}] {pos}/{len}"
            },
        )
        .unwrap()
        .progress_chars("=> "),
    );
    pb.set_prefix("Building");

    // process in another thread
    // crates to be iterated but not exactly a tree
    let crates = Arc::new(Mutex::new(CRATES.iter()));
    let (tx, rx) = mpsc::channel();
    for n in 0..NUM_CPUS {
        let tx = tx.clone();
        let crates = crates.clone();
        thread::spawn(move || {
            let mut rng = rand::rng();
            loop {
                let krate = crates.lock().unwrap().next();
                // notify main thread if n thread is processing a crate
                tx.send((n, krate)).unwrap();
                if let Some(krate) = krate {
                    thread::sleep(Duration::from_millis(
                        // last compile and linking is always slow, let's mimic that
                        if CRATES.last() == Some(krate) {
                            rng.random_range(1_000..2_000)
                        } else {
                            rng.random_range(250..1_000)
                        },
                    ));
                } else {
                    break;
                }
            }
        });
    }
    // drop tx to stop waiting
    drop(tx);

    let green_bold = Style::new().green().bold();

    // do progress drawing in main thread
    let mut processing = [None; NUM_CPUS];
    while let Ok((n, krate)) = rx.recv() {
        processing[n] = krate;
        let crates: Vec<&str> = processing
            .iter()
            .filter_map(|t| t.copied().map(|(name, _)| name))
            .collect();
        pb.set_message(crates.join(", "));
        if let Some((name, version)) = krate {
            // crate is being built
            let line = format!(
                "{:>12} {} {}",
                green_bold.apply_to("Compiling"),
                name,
                version
            );
            pb.println(line);

            pb.inc(1);
        }
    }
    pb.finish_and_clear();

    // compilation is finished
    println!(
        "{:>12} dev [unoptimized + debuginfo] target(s) in {}",
        green_bold.apply_to("Finished"),
        HumanDuration(start.elapsed())
    );
}
