use std::collections::{BTreeSet, HashSet};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};

use log::debug;

use super::{fingerprint, Context, Unit};
use crate::util::paths;
use crate::util::{internal, CargoResult};

fn render_filename<P: AsRef<Path>>(path: P, basedir: Option<&str>) -> CargoResult<String> {
    let path = path.as_ref();
    let relpath = match basedir {
        None => path,
        Some(base) => match path.strip_prefix(base) {
            Ok(relpath) => relpath,
            _ => path,
        },
    };
    relpath
        .to_str()
        .ok_or_else(|| internal("path not utf-8"))
        .map(|f| f.replace(" ", "\\ "))
}

fn add_deps_for_unit<'a, 'b>(
    deps: &mut BTreeSet<PathBuf>,
    context: &mut Context<'a, 'b>,
    unit: &Unit<'a>,
    visited: &mut HashSet<Unit<'a>>,
) -> CargoResult<()> {
    if !visited.insert(*unit) {
        return Ok(());
    }

    // units representing the execution of a build script don't actually
    // generate a dep info file, so we just keep on going below
    if !unit.mode.is_run_custom_build() {
        // Add dependencies from rustc dep-info output (stored in fingerprint directory)
        let dep_info_loc = fingerprint::dep_info_loc(context, unit);
        if let Some(paths) = fingerprint::parse_dep_info(unit.pkg, &dep_info_loc)? {
            for path in paths {
                deps.insert(path);
            }
        } else {
            debug!(
                "can't find dep_info for {:?} {}",
                unit.pkg.package_id(),
                unit.target
            );
            return Err(internal("dep_info missing"));
        }
    }

    // Add rerun-if-changed dependencies
    let key = (unit.pkg.package_id(), unit.kind);
    if let Some(output) = context.build_state.outputs.lock().unwrap().get(&key) {
        for path in &output.rerun_if_changed {
            deps.insert(path.into());
        }
    }

    // Recursively traverse all transitive dependencies
    for dep_unit in context.dep_targets(unit).iter() {
        let source_id = dep_unit.pkg.package_id().source_id();
        if source_id.is_path() {
            add_deps_for_unit(deps, context, dep_unit, visited)?;
        }
    }
    Ok(())
}

pub fn output_depinfo<'a, 'b>(cx: &mut Context<'a, 'b>, unit: &Unit<'a>) -> CargoResult<()> {
    let bcx = cx.bcx;
    let mut deps = BTreeSet::new();
    let mut visited = HashSet::new();
    let success = add_deps_for_unit(&mut deps, cx, unit, &mut visited).is_ok();
    let basedir_string;
    let basedir = match bcx.config.get_path("build.dep-info-basedir")? {
        Some(value) => {
            basedir_string = value
                .val
                .as_os_str()
                .to_str()
                .ok_or_else(|| internal("build.dep-info-basedir path not utf-8"))?
                .to_string();
            Some(basedir_string.as_str())
        }
        None => None,
    };
    let deps = deps
        .iter()
        .map(|f| render_filename(f, basedir))
        .collect::<CargoResult<Vec<_>>>()?;

    for output in cx.outputs(unit)?.iter() {
        if let Some(ref link_dst) = output.hardlink {
            let output_path = link_dst.with_extension("d");
            if success {
                let target_fn = render_filename(link_dst, basedir)?;

                // If nothing changed don't recreate the file which could alter
                // its mtime
                if let Ok(previous) = fingerprint::parse_rustc_dep_info(&output_path) {
                    if previous.len() == 1 && previous[0].0 == target_fn && previous[0].1 == deps {
                        continue;
                    }
                }

                // Otherwise write it all out
                let mut outfile = BufWriter::new(File::create(output_path)?);
                write!(outfile, "{}:", target_fn)?;
                for dep in &deps {
                    write!(outfile, " {}", dep)?;
                }
                writeln!(outfile)?;

            // dep-info generation failed, so delete output file. This will
            // usually cause the build system to always rerun the build
            // rule, which is correct if inefficient.
            } else if output_path.exists() {
                paths::remove_file(output_path)?;
            }
        }
    }
    Ok(())
}
