// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Types for parsing cargo.metadata JSON files.

use super::{Crate, CrateType, Extern, ExternType};
use crate::config::VariantConfig;
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

/// `cfg` strings for dependencies which should be considered enabled. It would be better to parse
/// them properly, but this is good enough in practice so far.
const ENABLED_CFGS: [&str; 6] = [
    r#"unix"#,
    r#"not(windows)"#,
    r#"any(unix, target_os = "wasi")"#,
    r#"not(all(target_family = "wasm", target_os = "unknown"))"#,
    r#"not(target_family = "wasm")"#,
    r#"any(target_os = "linux", target_os = "android")"#,
];

/// `cargo metadata` output.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct WorkspaceMetadata {
    pub packages: Vec<PackageMetadata>,
    pub workspace_members: Vec<String>,
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct PackageMetadata {
    pub name: String,
    pub version: String,
    pub edition: String,
    pub manifest_path: String,
    pub dependencies: Vec<DependencyMetadata>,
    pub features: BTreeMap<String, Vec<String>>,
    pub id: String,
    pub targets: Vec<TargetMetadata>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DependencyMetadata {
    pub name: String,
    pub kind: Option<String>,
    pub optional: bool,
    pub target: Option<String>,
    pub rename: Option<String>,
}

impl DependencyMetadata {
    /// Returns whether the dependency should be included when the given features are enabled.
    fn enabled(&self, features: &[String], cfgs: &[String]) -> bool {
        if let Some(target) = &self.target {
            if target.starts_with("cfg(") && target.ends_with(')') {
                let target_cfg = &target[4..target.len() - 1];
                if !ENABLED_CFGS.contains(&target_cfg) && !cfgs.contains(&target_cfg.to_string()) {
                    return false;
                }
            }
        }
        let name = self.rename.as_ref().unwrap_or(&self.name);
        !self.optional || features.contains(&format!("dep:{}", name))
    }
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
#[allow(dead_code)]
pub struct TargetMetadata {
    pub crate_types: Vec<CrateType>,
    pub doc: bool,
    pub doctest: bool,
    pub edition: String,
    pub kind: Vec<TargetKind>,
    pub name: String,
    pub src_path: PathBuf,
    pub test: bool,
}

#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde[rename_all = "kebab-case"]]
pub enum TargetKind {
    Bin,
    CustomBuild,
    Bench,
    Example,
    Lib,
    Rlib,
    Staticlib,
    Cdylib,
    ProcMacro,
    Test,
}

pub fn parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>> {
    let metadata =
        serde_json::from_str(cargo_metadata).context("failed to parse cargo metadata")?;
    parse_cargo_metadata(&metadata, &cfg.features, &cfg.extra_cfg, cfg.tests)
}

fn parse_cargo_metadata(
    metadata: &WorkspaceMetadata,
    features: &Option<Vec<String>>,
    cfgs: &[String],
    include_tests: bool,
) -> Result<Vec<Crate>> {
    let mut crates = Vec::new();
    for package in &metadata.packages {
        if !metadata.workspace_members.contains(&package.id) {
            continue;
        }

        let features = resolve_features(features, &package.features, &package.dependencies);
        let features_without_deps: Vec<String> =
            features.clone().into_iter().filter(|feature| !feature.starts_with("dep:")).collect();
        let package_dir = package_dir_from_id(&package.id)?;

        for target in &package.targets {
            let target_kinds = target
                .kind
                .clone()
                .into_iter()
                .filter(|kind| {
                    [
                        TargetKind::Bin,
                        TargetKind::Cdylib,
                        TargetKind::Lib,
                        TargetKind::ProcMacro,
                        TargetKind::Rlib,
                        TargetKind::Staticlib,
                        TargetKind::Test,
                    ]
                    .contains(kind)
                })
                .collect::<Vec<_>>();
            if target_kinds.is_empty() {
                // Only binaries, libraries and integration tests are supported.
                continue;
            }
            let main_src = split_src_path(&target.src_path, &package_dir);
            // Hypens are not allowed in crate names. See
            // https://github.com/rust-lang/rfcs/blob/master/text/0940-hyphens-considered-harmful.md
            // for background.
            let target_name = target.name.replace('-', "_");
            let target_triple = if target_kinds == [TargetKind::ProcMacro] {
                None
            } else {
                Some("x86_64-unknown-linux-gnu".to_string())
            };
            // Don't generate an entry for integration tests, they will be covered by the test case
            // below.
            if target_kinds != [TargetKind::Test] {
                crates.push(Crate {
                    name: target_name.clone(),
                    package_name: package.name.to_owned(),
                    version: Some(package.version.to_owned()),
                    types: target.crate_types.clone(),
                    features: features_without_deps.clone(),
                    edition: package.edition.to_owned(),
                    package_dir: package_dir.clone(),
                    main_src: main_src.to_owned(),
                    target: target_triple.clone(),
                    externs: get_externs(
                        package,
                        &metadata.packages,
                        &features,
                        cfgs,
                        &target_kinds,
                        false,
                    )?,
                    cfgs: cfgs.to_owned(),
                    ..Default::default()
                });
            }
            // This includes both unit tests and integration tests.
            if target.test && include_tests {
                crates.push(Crate {
                    name: target_name,
                    package_name: package.name.to_owned(),
                    version: Some(package.version.to_owned()),
                    types: vec![CrateType::Test],
                    features: features_without_deps.clone(),
                    edition: package.edition.to_owned(),
                    package_dir: package_dir.clone(),
                    main_src: main_src.to_owned(),
                    target: target_triple.clone(),
                    externs: get_externs(
                        package,
                        &metadata.packages,
                        &features,
                        cfgs,
                        &target_kinds,
                        true,
                    )?,
                    cfgs: cfgs.to_owned(),
                    ..Default::default()
                });
            }
        }
    }
    Ok(crates)
}

fn get_externs(
    package: &PackageMetadata,
    packages: &[PackageMetadata],
    features: &[String],
    cfgs: &[String],
    target_kinds: &[TargetKind],
    test: bool,
) -> Result<Vec<Extern>> {
    let mut externs = package
        .dependencies
        .iter()
        .filter_map(|dependency| {
            // Kind is None for normal dependencies, as opposed to dev dependencies.
            if dependency.enabled(features, cfgs)
                && dependency.kind.as_deref() != Some("build")
                && (dependency.kind.is_none() || test)
            {
                Some(make_extern(packages, dependency))
            } else {
                None
            }
        })
        .collect::<Result<Vec<Extern>>>()?;

    // If there is a library target and this is a binary or integration test, add the library as an
    // extern.
    if matches!(target_kinds, [TargetKind::Bin] | [TargetKind::Test]) {
        for target in &package.targets {
            if target.kind.contains(&TargetKind::Lib) {
                let lib_name = target.name.replace('-', "_");
                externs.push(Extern {
                    name: lib_name.clone(),
                    lib_name,
                    raw_name: target.name.clone(),
                    extern_type: ExternType::Rust,
                });
            }
        }
    }

    externs.sort();
    externs.dedup();
    Ok(externs)
}

fn make_extern(packages: &[PackageMetadata], dependency: &DependencyMetadata) -> Result<Extern> {
    let Some(package) = packages.iter().find(|package| package.name == dependency.name) else {
        bail!("package {} not found in metadata", dependency.name);
    };
    let Some(target) = package.targets.iter().find(|target| {
        target.kind.contains(&TargetKind::Lib) || target.kind.contains(&TargetKind::ProcMacro)
    }) else {
        bail!("Package {} didn't have any library or proc-macro targets", dependency.name);
    };
    let lib_name = target.name.replace('-', "_");
    let raw_name = target.name.clone();
    let name =
        if let Some(rename) = &dependency.rename { rename.clone() } else { lib_name.clone() };

    // Check whether the package is a proc macro.
    let extern_type =
        if package.targets.iter().any(|target| target.kind.contains(&TargetKind::ProcMacro)) {
            ExternType::ProcMacro
        } else {
            ExternType::Rust
        };

    Ok(Extern { name, lib_name, raw_name, extern_type })
}

/// Given a Cargo package ID, returns the path.
///
/// Extracts `"/path/to/crate"` from
/// `"path+file:///path/to/crate#1.2.3"`. See
/// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html for
/// information on Cargo package ID specifications.
fn package_dir_from_id(id: &str) -> Result<PathBuf> {
    const PREFIX: &str = "path+file://";
    const SEPARATOR: char = '#';
    let Some(stripped) = id.strip_prefix(PREFIX) else {
        bail!("Invalid package ID {id:?}, expected it to start with {PREFIX:?}");
    };
    let Some(idx) = stripped.rfind(SEPARATOR) else {
        bail!("Invalid package ID {id:?}, expected it to contain {SEPARATOR:?}");
    };
    Ok(PathBuf::from(stripped[..idx].to_string()))
}

fn split_src_path<'a>(src_path: &'a Path, package_dir: &Path) -> &'a Path {
    if let Ok(main_src) = src_path.strip_prefix(package_dir) {
        main_src
    } else {
        src_path
    }
}

/// Given a set of chosen features, and the feature dependencies from a package's metadata, returns
/// the full set of features which should be enabled.
fn resolve_features(
    chosen_features: &Option<Vec<String>>,
    package_features: &BTreeMap<String, Vec<String>>,
    dependencies: &[DependencyMetadata],
) -> Vec<String> {
    let mut package_features = package_features.to_owned();
    // Add implicit features for optional dependencies.
    for dependency in dependencies {
        if dependency.optional && !package_features.contains_key(&dependency.name) {
            package_features
                .insert(dependency.name.to_owned(), vec![format!("dep:{}", dependency.name)]);
        }
    }

    let mut features = Vec::new();
    if let Some(chosen_features) = chosen_features {
        for feature in chosen_features {
            add_feature_and_dependencies(&mut features, feature, &package_features);
        }
    } else {
        // If there are no chosen features, then enable the default feature.
        add_feature_and_dependencies(&mut features, "default", &package_features);
    }
    features.sort();
    features.dedup();
    features
}

/// Adds the given feature and all features it depends on to the given list of features.
///
/// Ignores features of other packages, and features which don't exist.
fn add_feature_and_dependencies(
    features: &mut Vec<String>,
    feature: &str,
    package_features: &BTreeMap<String, Vec<String>>,
) {
    if package_features.contains_key(feature) || feature.starts_with("dep:") {
        features.push(feature.to_owned());
    }

    if let Some(dependencies) = package_features.get(feature) {
        for dependency in dependencies {
            if let Some((dependency_package, _)) = dependency.split_once('/') {
                add_feature_and_dependencies(features, dependency_package, package_features);
            } else {
                add_feature_and_dependencies(features, dependency, package_features);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::Config;
    use crate::tests::testdata_directories;
    use googletest::matchers::eq;
    use googletest::prelude::assert_that;
    use std::fs::{read_to_string, File};

    #[test]
    fn extract_package_dir_from_id() -> Result<()> {
        assert_eq!(
            package_dir_from_id("path+file:///path/to/crate#1.2.3")?,
            PathBuf::from("/path/to/crate")
        );
        Ok(())
    }

    #[test]
    fn resolve_multi_level_feature_dependencies() {
        let chosen = vec!["default".to_string(), "extra".to_string(), "on_by_default".to_string()];
        let package_features = [
            (
                "default".to_string(),
                vec!["std".to_string(), "other".to_string(), "on_by_default".to_string()],
            ),
            ("std".to_string(), vec!["alloc".to_string()]),
            ("not_enabled".to_string(), vec![]),
            ("on_by_default".to_string(), vec![]),
            ("other".to_string(), vec![]),
            ("extra".to_string(), vec![]),
            ("alloc".to_string(), vec![]),
        ]
        .into_iter()
        .collect();
        assert_eq!(
            resolve_features(&Some(chosen), &package_features, &[]),
            vec![
                "alloc".to_string(),
                "default".to_string(),
                "extra".to_string(),
                "on_by_default".to_string(),
                "other".to_string(),
                "std".to_string(),
            ]
        );
    }

    #[test]
    fn resolve_dep_features() {
        let package_features = [(
            "default".to_string(),
            vec![
                "optionaldep/feature".to_string(),
                "requireddep/feature".to_string(),
                "optionaldep2?/feature".to_string(),
            ],
        )]
        .into_iter()
        .collect();
        let dependencies = vec![
            DependencyMetadata {
                name: "optionaldep".to_string(),
                kind: None,
                optional: true,
                target: None,
                rename: None,
            },
            DependencyMetadata {
                name: "optionaldep2".to_string(),
                kind: None,
                optional: true,
                target: None,
                rename: None,
            },
            DependencyMetadata {
                name: "requireddep".to_string(),
                kind: None,
                optional: false,
                target: None,
                rename: None,
            },
        ];
        assert_eq!(
            resolve_features(&None, &package_features, &dependencies),
            vec!["default".to_string(), "dep:optionaldep".to_string(), "optionaldep".to_string()]
        );
    }

    #[test]
    fn get_externs_cfg() {
        let package = PackageMetadata {
            name: "test_package".to_string(),
            dependencies: vec![
                DependencyMetadata {
                    name: "alwayslib".to_string(),
                    kind: None,
                    optional: false,
                    target: None,
                    rename: None,
                },
                DependencyMetadata {
                    name: "unixlib".to_string(),
                    kind: None,
                    optional: false,
                    target: Some("cfg(unix)".to_string()),
                    rename: None,
                },
                DependencyMetadata {
                    name: "windowslib".to_string(),
                    kind: None,
                    optional: false,
                    target: Some("cfg(windows)".to_string()),
                    rename: None,
                },
            ],
            features: [].into_iter().collect(),
            targets: vec![],
            ..Default::default()
        };
        let packages = vec![
            package.clone(),
            PackageMetadata {
                name: "alwayslib".to_string(),
                targets: vec![TargetMetadata {
                    name: "alwayslib".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
            PackageMetadata {
                name: "unixlib".to_string(),
                targets: vec![TargetMetadata {
                    name: "unixlib".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
            PackageMetadata {
                name: "windowslib".to_string(),
                targets: vec![TargetMetadata {
                    name: "windowslib".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
        ];
        assert_eq!(
            get_externs(&package, &packages, &[], &[], &[], false).unwrap(),
            vec![
                Extern {
                    name: "alwayslib".to_string(),
                    lib_name: "alwayslib".to_string(),
                    raw_name: "alwayslib".to_string(),
                    extern_type: ExternType::Rust
                },
                Extern {
                    name: "unixlib".to_string(),
                    lib_name: "unixlib".to_string(),
                    raw_name: "unixlib".to_string(),
                    extern_type: ExternType::Rust
                },
            ]
        );
    }

    #[test]
    fn get_externs_extra_cfg() {
        let package = PackageMetadata {
            name: "test_package".to_string(),
            dependencies: vec![
                DependencyMetadata {
                    name: "foolib".to_string(),
                    kind: None,
                    optional: false,
                    target: Some("cfg(foo)".to_string()),
                    rename: None,
                },
                DependencyMetadata {
                    name: "barlib".to_string(),
                    kind: None,
                    optional: false,
                    target: Some("cfg(bar)".to_string()),
                    rename: None,
                },
            ],
            features: [].into_iter().collect(),
            targets: vec![],
            ..Default::default()
        };
        let packages = vec![
            package.clone(),
            PackageMetadata {
                name: "foolib".to_string(),
                targets: vec![TargetMetadata {
                    name: "foolib".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
            PackageMetadata {
                name: "barlib".to_string(),
                targets: vec![TargetMetadata {
                    name: "barlib".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
        ];
        assert_eq!(
            get_externs(&package, &packages, &[], &["foo".to_string()], &[], false).unwrap(),
            vec![Extern {
                name: "foolib".to_string(),
                lib_name: "foolib".to_string(),
                raw_name: "foolib".to_string(),
                extern_type: ExternType::Rust
            },]
        );
    }

    #[test]
    fn get_externs_rename() {
        let package = PackageMetadata {
            name: "test_package".to_string(),
            dependencies: vec![
                DependencyMetadata {
                    name: "foo".to_string(),
                    kind: None,
                    optional: false,
                    target: None,
                    rename: Some("foo2".to_string()),
                },
                DependencyMetadata {
                    name: "bar".to_string(),
                    kind: None,
                    optional: true,
                    target: None,
                    rename: None,
                },
                DependencyMetadata {
                    name: "bar".to_string(),
                    kind: None,
                    optional: true,
                    target: None,
                    rename: Some("baz".to_string()),
                },
            ],
            ..Default::default()
        };
        let packages = vec![
            package.clone(),
            PackageMetadata {
                name: "foo".to_string(),
                targets: vec![TargetMetadata {
                    name: "foo".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
            PackageMetadata {
                name: "bar".to_string(),
                targets: vec![TargetMetadata {
                    name: "bar".to_string(),
                    kind: vec![TargetKind::Lib],
                    ..Default::default()
                }],
                ..Default::default()
            },
        ];
        assert_eq!(
            get_externs(&package, &packages, &["dep:bar".to_string()], &[], &[], false).unwrap(),
            vec![
                Extern {
                    name: "bar".to_string(),
                    lib_name: "bar".to_string(),
                    raw_name: "bar".to_string(),
                    extern_type: ExternType::Rust
                },
                Extern {
                    name: "foo2".to_string(),
                    lib_name: "foo".to_string(),
                    raw_name: "foo".to_string(),
                    extern_type: ExternType::Rust
                },
            ]
        );
        assert_eq!(
            get_externs(&package, &packages, &["dep:baz".to_string()], &[], &[], false).unwrap(),
            vec![
                Extern {
                    name: "baz".to_string(),
                    lib_name: "bar".to_string(),
                    raw_name: "bar".to_string(),
                    extern_type: ExternType::Rust
                },
                Extern {
                    name: "foo2".to_string(),
                    lib_name: "foo".to_string(),
                    raw_name: "foo".to_string(),
                    extern_type: ExternType::Rust
                },
            ]
        );
    }

    #[test]
    fn parse_metadata() {
        /// Remove anything before "external/rust/crates/" from the
        /// `package_dir` field. This makes the test robust since you
        /// can use `cargo metadata` to regenerate the test files and
        /// you don't have to care about where your AOSP checkout
        /// lives.
        fn normalize_package_dir(mut c: Crate) -> Crate {
            const EXTERNAL_RUST_CRATES: &str = "external/rust/crates/";
            let package_dir = c.package_dir.to_str().unwrap();
            if let Some(idx) = package_dir.find(EXTERNAL_RUST_CRATES) {
                c.package_dir = PathBuf::from(format!(".../{}", &package_dir[idx..]));
            }
            c
        }

        for testdata_directory_path in testdata_directories() {
            let cfg = Config::from_json_str(
                &read_to_string(testdata_directory_path.join("cargo_embargo.json"))
                    .with_context(|| {
                        format!(
                            "Failed to open {:?}",
                            testdata_directory_path.join("cargo_embargo.json")
                        )
                    })
                    .unwrap(),
            )
            .unwrap();
            let cargo_metadata_path = testdata_directory_path.join("cargo.metadata");
            let expected_crates: Vec<Vec<Crate>> = serde_json::from_reader::<_, Vec<Vec<Crate>>>(
                File::open(testdata_directory_path.join("crates.json")).unwrap(),
            )
            .unwrap()
            .into_iter()
            .map(|crates: Vec<Crate>| crates.into_iter().map(normalize_package_dir).collect())
            .collect();

            let crates = cfg
                .variants
                .iter()
                .map(|variant_cfg| {
                    parse_cargo_metadata_str(
                        &read_to_string(&cargo_metadata_path)
                            .with_context(|| format!("Failed to open {:?}", cargo_metadata_path))
                            .unwrap(),
                        variant_cfg,
                    )
                    .unwrap()
                    .into_iter()
                    .map(normalize_package_dir)
                    .collect::<Vec<Crate>>()
                })
                .collect::<Vec<Vec<Crate>>>();
            assert_that!(format!("{crates:#?}"), eq(format!("{expected_crates:#?}")));
        }
    }
}
