extern crate autotools;

use SHA256Status::{Mismatch, Unknown};
use autotools::Config;
use std::env;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::process::Command;

const PACKAGE_URL: &str =
    "https://github.com/cuddorg/cudd/releases/download/3.0.0/cudd-3.0.0.tar.gz";
const PACKAGE_SHA256: &str = "5fe145041c594689e6e7cf4cd623d5f2b7c36261708be8c9a72aed72cf67acce";

#[derive(Debug)]
#[allow(dead_code)]
enum FetchError {
    CommandError(std::process::ExitStatus),
    IOError(std::io::Error),
    PathExists,
}

enum SHA256Status {
    Match,
    Mismatch,
    Unknown,
}

impl From<std::io::Error> for FetchError {
    fn from(err: std::io::Error) -> FetchError {
        FetchError::IOError(err)
    }
}

/// Run a command and return (stdout, stderr) if exit status is success.
fn run_command(cmd: &mut Command) -> Result<(String, String), FetchError> {
    let output = cmd.output()?;

    if output.status.success() {
        Ok((
            String::from_utf8(output.stdout).unwrap(),
            String::from_utf8(output.stderr).unwrap(),
        ))
    } else {
        eprintln!("Command {:?} exited with status {}", cmd, output.status);
        Err(FetchError::CommandError(output.status))
    }
}

/// Fetch a file from a URL if it does not already exist in out_dir and verify its sha256sum if possible.
fn fetch_package(
    out_dir: &str,
    url: &str,
    sha256: &str,
) -> Result<(PathBuf, SHA256Status), FetchError> {
    let out_path = Path::new(&out_dir);
    let target_path = out_path.join(Path::new(url).file_name().unwrap());
    let target_path_str = target_path.clone().into_os_string().into_string().unwrap();

    match target_path.metadata() {
        Err(error) if error.kind() == ErrorKind::NotFound => {
            // Path does not exist! Start download...
            println!("Downloading {} to {}", url, target_path_str);
            let mut command = Command::new("curl");
            command.args(["-L", url, "-o", target_path_str.as_str()]);
            run_command(&mut command)?;
        }
        Ok(data) if data.is_file() => {
            println!("{} exists. Skipping download.", target_path_str);
        }
        Ok(_) => return Err(FetchError::PathExists),
        Err(error) => return Err(FetchError::from(error)),
    }

    // Now run sha256 sum check:
    let mut command_1 = Command::new("sha256sum");
    command_1.arg(target_path.clone());
    let mut command_2 = Command::new("shasum -a 256");
    command_2.arg(target_path.clone());
    let sha256_result = run_command(&mut command_1).or_else(|_| run_command(&mut command_2));

    let sha256_status = match sha256_result {
        Err(_) => SHA256Status::Unknown,
        Ok((output, _)) if output.contains(sha256) => SHA256Status::Match,
        _ => SHA256Status::Mismatch,
    };

    Ok((target_path, sha256_status))
}

fn main() -> Result<(), String> {
    let build_cudd = env::var_os("CARGO_FEATURE_BUILD_CUDD").is_some();
    if !build_cudd {
        // If silent build is active, don't do anything.
        return Ok(());
    }

    let out_dir = env::var("OUT_DIR")
        .map_err(|_| "Environmental variable `OUT_DIR` not defined.".to_string())?;

    let (tar_path, sha256_status) = fetch_package(&out_dir, PACKAGE_URL, PACKAGE_SHA256)
        .map_err(|e| format!("Error downloading CUDD package: {:?}.", e))?;
    let tar_path_str = tar_path.to_str().unwrap().to_string();

    match sha256_status {
        Unknown => eprintln!("WARNING: SHA256 not computed. Package validation skipped."),
        Mismatch => return Err("CUDD package SHA256 hash mismatch.".to_string()),
        _ => (),
    }

    // Get cudd.tar.gz path without extensions.
    let cudd_path = tar_path.with_extension("").with_extension("");
    let cudd_path_str = cudd_path.clone().into_os_string().into_string().unwrap();

    if !cudd_path.exists() {
        // Create the destination directory.
        std::fs::create_dir_all(cudd_path.clone())
            .map_err(|e| format!("Cannot create CUDD directory: {:?}", e))?;
    }

    // un-tar package, ignoring the name of the top level folder, dumping into cudd_path instead.
    let mut tar_command = Command::new("tar");
    tar_command.args([
        "xf",
        &tar_path_str,
        "--strip-components=1",
        "-C",
        &cudd_path_str,
    ]);
    run_command(&mut tar_command).map_err(|e| format!("Error decompressing CUDD: {:?}", e))?;

    // Enable dddmp when building.
    let build_output = Config::new(cudd_path).enable("dddmp", None).build();

    println!(
        "cargo:rustc-link-search=native={}",
        build_output.join("lib").display()
    );
    println!("cargo:rustc-link-lib=static=cudd");

    Ok(())
}
