#![deny(missing_docs)]
#![allow(dead_code)]
//! Structured access to the output of `cargo metadata`
//! Usually used from within a `cargo-*` executable

// Forked from `https://github.com/oli-obk/cargo_metadata`
// Modifications:
//   1. Remove `resolve` from Metadata because it was causing parse failures
//   2. Fix the `manifest-path` argument
//   3. Add `--all-features` argument
//   4. Remove the `--no-deps` argument

use std::borrow::{Borrow, Cow};
use std::collections::{HashMap, HashSet};
use std::env;
use std::error;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::io;
use std::path::Path;
use std::process::{Command, Output};
use std::str::Utf8Error;

#[derive(Clone, Deserialize, Debug)]
/// Starting point for metadata returned by `cargo metadata`
pub struct Metadata {
    /// A list of all crates referenced by this crate (and the crate itself)
    pub packages: HashSet<Package>,
    version: usize,
    /// path to the workspace containing the `Cargo.lock`
    pub workspace_root: String,
}

/// A reference to a package including it's name and the specific version.
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct PackageRef {
    pub name: String,
    pub version: Option<String>,
}

#[derive(Clone, Deserialize, Debug)]
/// A crate
pub struct Package {
    #[serde(flatten)]
    pub name_and_version: PackageRef,
    id: String,
    source: Option<String>,
    /// List of dependencies of this particular package
    pub dependencies: HashSet<Dependency>,
    /// Targets provided by the crate (lib, bin, example, test, ...)
    pub targets: Vec<Target>,
    features: HashMap<String, Vec<String>>,
    /// path containing the `Cargo.toml`
    pub manifest_path: String,
}

#[derive(Clone, Deserialize, Debug)]
/// A dependency of the main crate
pub struct Dependency {
    /// Name as given in the `Cargo.toml`
    pub name: String,
    source: Option<String>,
    /// Whether this is required or optional
    pub req: String,
    kind: Option<String>,
    optional: bool,
    uses_default_features: bool,
    features: Vec<String>,
    pub target: Option<String>,
}

#[derive(Clone, Deserialize, Debug)]
/// A single target (lib, bin, example, ...) provided by a crate
pub struct Target {
    /// Name as given in the `Cargo.toml` or generated from the file name
    pub name: String,
    /// Kind of target ("bin", "example", "test", "bench", "lib")
    pub kind: Vec<String>,
    /// Almost the same as `kind`, except when an example is a library instad of an executable.
    /// In that case `crate_types` contains things like `rlib` and `dylib` while `kind` is `example`
    #[serde(default)]
    pub crate_types: Vec<String>,
    /// Path to the main source file of the target
    pub src_path: String,
}

#[derive(Debug)]
/// Possible errors that can occur during metadata parsing.
pub enum Error {
    /// Error during execution of `cargo metadata`
    Io(io::Error),
    /// Metadata extraction failure
    Metadata(Output),
    /// Output of `cargo metadata` was not valid utf8
    Utf8(Utf8Error),
    /// Deserialization error (structure of json did not match expected structure)
    Json(serde_json::Error),
}

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 From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Self {
        Error::Json(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::Metadata(_) => write!(f, "Metadata error"),
            Error::Utf8(ref err) => err.fmt(f),
            Error::Json(ref err) => err.fmt(f),
        }
    }
}

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

// Implementations that let us lookup Packages and Dependencies by name (string)

impl Borrow<PackageRef> for Package {
    fn borrow(&self) -> &PackageRef {
        &self.name_and_version
    }
}

impl Hash for Package {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.name_and_version.hash(state);
    }
}

impl PartialEq for Package {
    fn eq(&self, other: &Self) -> bool {
        self.name_and_version == other.name_and_version
    }
}

impl Eq for Package {}

impl Borrow<str> for Dependency {
    fn borrow(&self) -> &str {
        &self.name
    }
}

impl Hash for Dependency {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

impl PartialEq for Dependency {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for Dependency {}

fn discover_target(manifest_path: &Path) -> Option<String> {
    if let Ok(target) = std::env::var("TARGET") {
        return Some(target);
    }

    // We must be running as a standalone script, not under cargo.
    // Let's use the host platform instead.
    // We figure out the host platform through rustc and use that.
    // We unfortunatelly cannot go through cargo, since cargo rustc _also_ builds.
    // If `rustc` fails to run, we just fall back to not passing --filter-platforms.
    //
    // NOTE: We set the current directory in case of rustup shenanigans.
    let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc"));
    debug!("Discovering host platform by {rustc:?}");

    let rustc_output = Command::new(rustc)
        .current_dir(manifest_path.parent().unwrap())
        .arg("-vV")
        .output();
    let rustc_output = match rustc_output {
        Ok(ref out) => String::from_utf8_lossy(&out.stdout),
        Err(..) => return None,
    };

    let field = "host: ";
    rustc_output
        .lines()
        .find_map(|l| l.strip_prefix(field).map(|stripped| stripped.to_string()))
}

/// The main entry point to obtaining metadata
pub fn metadata(
    manifest_path: &Path,
    existing_metadata_file: Option<&Path>,
    only_target: bool,
) -> Result<Metadata, Error> {
    let output;
    let metadata = match existing_metadata_file {
        Some(path) => Cow::Owned(std::fs::read_to_string(path)?),
        None => {
            let target = if only_target {
                let target = discover_target(manifest_path);
                if target.is_none() {
                    warn!(
                        "Failed to discover host platform for cargo metadata; \
                        will fetch dependencies for all platforms."
                    );
                }
                target
            } else {
                None
            };

            let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo"));
            let mut cmd = Command::new(cargo);
            cmd.arg("metadata");
            cmd.arg("--all-features");
            cmd.arg("--format-version").arg("1");
            if let Some(target) = target {
                cmd.arg("--filter-platform").arg(target);
            }
            cmd.arg("--manifest-path");
            cmd.arg(manifest_path);
            output = cmd.output()?;
            if !output.status.success() {
                return Err(Error::Metadata(output));
            }
            Cow::Borrowed(std::str::from_utf8(&output.stdout)?)
        }
    };

    let meta: Metadata = serde_json::from_str(&metadata)?;
    Ok(meta)
}
