use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;

#[derive(Default)]
pub struct Crate {
    pub include_prefix: Option<PathBuf>,
    pub links: Option<OsString>,
    pub header_dirs: Vec<HeaderDir>,
}

pub struct HeaderDir {
    pub exported: bool,
    pub path: PathBuf,
}

impl Crate {
    pub fn print_to_cargo(&self) {
        if let Some(include_prefix) = &self.include_prefix {
            println!(
                "cargo:CXXBRIDGE_PREFIX={}",
                include_prefix.to_string_lossy(),
            );
        }
        if let Some(links) = &self.links {
            println!("cargo:CXXBRIDGE_LINKS={}", links.to_string_lossy());
        }
        for (i, header_dir) in self.header_dirs.iter().enumerate() {
            if header_dir.exported {
                println!(
                    "cargo:CXXBRIDGE_DIR{}={}",
                    i,
                    header_dir.path.to_string_lossy(),
                );
            }
        }
    }
}

pub fn direct_dependencies() -> Vec<Crate> {
    let mut crates: BTreeMap<String, Crate> = BTreeMap::new();
    let mut exported_header_dirs: BTreeMap<String, Vec<(usize, PathBuf)>> = BTreeMap::new();

    // Only variables set from a build script of direct dependencies are
    // observable. That's exactly what we want! Your crate needs to declare a
    // direct dependency on the other crate in order to be able to #include its
    // headers.
    //
    // Also, they're only observable if the dependency's manifest contains a
    // `links` key. This is important because Cargo imposes no ordering on the
    // execution of build scripts without a `links` key. When exposing a
    // generated header for the current crate to #include, we need to be sure
    // the dependency's build script has already executed and emitted that
    // generated header.
    //
    // References:
    //   - https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key
    //   - https://doc.rust-lang.org/cargo/reference/build-script-examples.html#using-another-sys-crate
    for (k, v) in env::vars_os() {
        let mut k = k.to_string_lossy().into_owned();
        if !k.starts_with("DEP_") {
            continue;
        }

        if k.ends_with("_CXXBRIDGE_PREFIX") {
            k.truncate(k.len() - "_CXXBRIDGE_PREFIX".len());
            crates.entry(k).or_default().include_prefix = Some(PathBuf::from(v));
            continue;
        }

        if k.ends_with("_CXXBRIDGE_LINKS") {
            k.truncate(k.len() - "_CXXBRIDGE_LINKS".len());
            crates.entry(k).or_default().links = Some(v);
            continue;
        }

        let without_counter = k.trim_end_matches(|ch: char| ch.is_ascii_digit());
        let counter_len = k.len() - without_counter.len();
        if counter_len == 0 || !without_counter.ends_with("_CXXBRIDGE_DIR") {
            continue;
        }

        let sort_key = k[k.len() - counter_len..]
            .parse::<usize>()
            .unwrap_or(usize::MAX);
        k.truncate(k.len() - counter_len - "_CXXBRIDGE_DIR".len());
        exported_header_dirs
            .entry(k)
            .or_default()
            .push((sort_key, PathBuf::from(v)));
    }

    for (k, mut dirs) in exported_header_dirs {
        dirs.sort_by_key(|(sort_key, _dir)| *sort_key);
        crates
            .entry(k)
            .or_default()
            .header_dirs
            .extend(dirs.into_iter().map(|(_sort_key, dir)| HeaderDir {
                exported: true,
                path: dir,
            }));
    }

    crates.into_iter().map(|entry| entry.1).collect()
}
