//! Core of cargo-remove command

use crate::CargoResult;
use crate::GlobalContext;
use crate::core::Package;
use crate::util::toml_mut::manifest::DepTable;
use crate::util::toml_mut::manifest::LocalManifest;
use crate::util::toml_mut::manifest::MissingDependencyError;

/// Remove a dependency from a Cargo.toml manifest file.
#[derive(Debug)]
pub struct RemoveOptions<'a> {
    /// Configuration information for Cargo operations
    pub gctx: &'a GlobalContext,
    /// Package to remove dependencies from
    pub spec: &'a Package,
    /// Dependencies to remove
    pub dependencies: Vec<String>,
    /// Which dependency section to remove these from
    pub section: DepTable,
    /// Whether or not to actually write the manifest
    pub dry_run: bool,
}

/// Remove dependencies from a manifest
pub fn remove(options: &RemoveOptions<'_>) -> CargoResult<()> {
    let dep_table = options
        .section
        .to_table()
        .into_iter()
        .map(String::from)
        .collect::<Vec<_>>();

    let manifest_path = options.spec.manifest_path().to_path_buf();
    let mut manifest = LocalManifest::try_new(&manifest_path)?;

    for dep in &options.dependencies {
        let section = if dep_table.len() >= 3 {
            format!("{} for target `{}`", &dep_table[2], &dep_table[1])
        } else {
            dep_table[0].clone()
        };
        options
            .gctx
            .shell()
            .status("Removing", format!("{dep} from {section}"))?;

        manifest.remove_from_table(&dep_table, dep).map_err(
            |MissingDependencyError {
                 expected_name,
                 expected_path,
                 alt_name,
                 alt_path,
             }| {
                use std::fmt::Write as _;

                let mut error = String::new();
                let path = expected_path.join(".");
                let _ = write!(
                    &mut error,
                    "the dependency `{expected_name}` could not be found in `{path}`"
                );
                if let Some(alt_path) = alt_path {
                    let mut flags = Vec::new();
                    match (
                        expected_path.last().unwrap().as_str(),
                        alt_path.last().unwrap().as_str(),
                    ) {
                        ("build-dependencies", "build-dependencies") => {}
                        ("dev-dependencies", "dev-dependencies") => {}
                        ("dependencies", "dependencies") => {}
                        (_, "build-dependencies") => flags.push("--build"),
                        (_, "dev-dependencies") => flags.push("--dev"),
                        (_, _) => {}
                    }
                    if expected_path[0] != "target" && alt_path[0] == "target" {
                        flags.push(&"--target");
                        flags.push(&alt_path[1]);
                    }
                    let alt_path = alt_path.join(".");
                    if !flags.is_empty() {
                        let flags = flags.join(" ");
                        let _ = write!(
                            &mut error,
                            "\n\nhelp: pass `{flags}` to remove `{expected_name}` from `{alt_path}`"
                        );
                    } else {
                        let _ = write!(
                            &mut error,
                            "\n\nhelp: a dependency with the same name exists in `{alt_path}`"
                        );
                    }
                } else if let Some(alt_name) = alt_name {
                    let _ = write!(
                        &mut error,
                        "\n\nhelp: a dependency with a similar name exists: `{alt_name}`"
                    );
                }
                anyhow::format_err!("{error}")
            },
        )?;

        // Now that we have removed the crate, if that was the last reference to that
        // crate, then we need to drop any explicitly activated features on
        // that crate.
        manifest.gc_dep(dep);
    }

    manifest.ensure_edition();

    if options.dry_run {
        options
            .gctx
            .shell()
            .warn("aborting remove due to dry run")?;
    } else {
        manifest.write()?;
    }

    Ok(())
}
