/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::bindgen::config::Profile;
use std::env;
use std::error;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::{from_utf8, Utf8Error};

extern crate tempfile;
use self::tempfile::Builder;

#[derive(Debug)]
/// Possible errors that can occur during `rustc -Zunpretty=expanded`.
pub enum Error {
    /// Error during creation of temporary directory
    Io(io::Error),
    /// Output of `cargo metadata` was not valid utf8
    Utf8(Utf8Error),
    /// Error during execution of `cargo rustc -Zunpretty=expanded`
    Compile(String),
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Self {
        Error::Io(err)
    }
}
impl From<Utf8Error> for Error {
    fn from(err: Utf8Error) -> Self {
        Error::Utf8(err)
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::Io(ref err) => err.fmt(f),
            Error::Utf8(ref err) => err.fmt(f),
            Error::Compile(ref err) => write!(f, "{err}"),
        }
    }
}

impl error::Error for Error {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self {
            Error::Io(ref err) => Some(err),
            Error::Utf8(ref err) => Some(err),
            Error::Compile(..) => None,
        }
    }
}

/// Use rustc to expand and pretty print the crate into a single file,
/// removing any macros in the process.
#[allow(clippy::too_many_arguments)]
pub fn expand(
    manifest_path: &Path,
    crate_name: &str,
    version: Option<&str>,
    use_tempdir: bool,
    expand_all_features: bool,
    expand_default_features: bool,
    expand_features: &Option<Vec<String>>,
    profile: Profile,
) -> Result<String, Error> {
    let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo"));
    let mut cmd = Command::new(cargo);

    let mut _temp_dir = None; // drop guard
    if use_tempdir {
        _temp_dir = Some(Builder::new().prefix("cbindgen-expand").tempdir()?);
        cmd.env("CARGO_TARGET_DIR", _temp_dir.unwrap().path());
    } else if let Ok(ref path) = env::var("CARGO_EXPAND_TARGET_DIR") {
        cmd.env("CARGO_TARGET_DIR", path);
    } else if let Ok(ref path) = env::var("OUT_DIR") {
        // When cbindgen was started programatically from a build.rs file, Cargo is running and
        // locking the default target directory. In this case we need to use another directory,
        // else we would end up in a deadlock. If Cargo is running `OUT_DIR` will be set, so we
        // can use a directory relative to that.
        cmd.env("CARGO_TARGET_DIR", PathBuf::from(path).join("expanded"));
    }

    // Set this variable so that we don't call it recursively if we expand a crate that is using
    // cbindgen
    cmd.env("_CBINDGEN_IS_RUNNING", "1");

    cmd.arg("rustc");
    cmd.arg("--lib");
    // When build with the release profile we can't choose the `check` profile.
    if profile != Profile::Release {
        cmd.arg("--profile=check");
    }
    cmd.arg("--manifest-path");
    cmd.arg(manifest_path);
    if let Some(features) = expand_features {
        cmd.arg("--features");
        let mut features_str = String::new();
        for (index, feature) in features.iter().enumerate() {
            if index != 0 {
                features_str.push(' ');
            }
            features_str.push_str(feature);
        }
        cmd.arg(features_str);
    }
    if expand_all_features {
        cmd.arg("--all-features");
    }
    if !expand_default_features {
        cmd.arg("--no-default-features");
    }
    match profile {
        Profile::Debug => {}
        Profile::Release => {
            cmd.arg("--release");
        }
    }
    cmd.arg("-p");
    let mut package = crate_name.to_owned();
    if let Some(version) = version {
        package.push(':');
        package.push_str(version);
    }
    cmd.arg(&package);
    cmd.arg("--verbose");
    cmd.arg("--");
    cmd.arg("-Zunpretty=expanded");
    info!("Command: {cmd:?}");
    let output = cmd.output()?;

    let src = from_utf8(&output.stdout)?.to_owned();
    let error = from_utf8(&output.stderr)?.to_owned();

    if src.is_empty() {
        Err(Error::Compile(error))
    } else {
        Ok(src)
    }
}
