use std::fmt::{Formatter as FFormatter, Result as FResult, Write as FWrite};
use serde::{Deserializer, Deserialize, Serializer, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::io::ErrorKind as IoErrorKind;
use std::process::Command;
use std::default::Default;
use semver::VersionReq;
use serde_json as json;
use std::borrow::Cow;
use std::path::Path;
use serde::de;
use std::fs;
use toml;


/// A single operation to be executed upon configuration of a package.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum ConfigOperation {
    /// Set the toolchain to use to compile the package.
    SetToolchain(String),
    /// Use the default toolchain to use to compile the package.
    RemoveToolchain,
    /// Whether to compile the package with the default features.
    DefaultFeatures(bool),
    /// Compile the package with the specified feature.
    AddFeature(String),
    /// Remove the feature from the list of features to compile with.
    RemoveFeature(String),
    /// Set build profile (`dev`/`release`/*~/.cargo/config.toml* `[profile.gaming]`/&c.)
    SetBuildProfile(Cow<'static, str>),
    /// Set allowing to install prereleases to the specified value.
    SetInstallPrereleases(bool),
    /// Set enforcing Cargo.lock to the specified value.
    SetEnforceLock(bool),
    /// Set installing only the pre-set binaries.
    SetRespectBinaries(bool),
    /// Constrain the installed to the specified one.
    SetTargetVersion(VersionReq),
    /// Always install latest package version.
    RemoveTargetVersion,
    /// Set environment variable to given value for `cargo install`.
    SetEnvironment(String, String),
    /// Remove environment variable for `cargo install`.
    ClearEnvironment(String),
    /// Remove configuration for an environment variable.
    InheritEnvironment(String),
    /// Reset configuration to default values.
    ResetConfig,
}


/// Compilation configuration for one crate.
///
/// # Examples
///
/// Reading a configset, adding an entry to it, then writing it back.
///
/// ```
/// # use cargo_update::ops::PackageConfig;
/// # use std::fs::{File, create_dir_all};
/// # use std::path::Path;
/// # use std::env::temp_dir;
/// # let td = temp_dir().join("cargo_update-doctest").join("PackageConfig-0");
/// # create_dir_all(&td).unwrap();
/// # let config_file = td.join(".install_config.toml");
/// # let operations = [];
/// let mut configuration = PackageConfig::read(&config_file, Path::new("/ENOENT")).unwrap();
/// configuration.insert("cargo_update".to_string(), PackageConfig::from(&operations));
/// PackageConfig::write(&configuration, &config_file).unwrap();
/// ```
#[derive(Debug, Clone, Hash, Eq, Serialize, Deserialize)]
pub struct PackageConfig {
    /// Toolchain to use to compile the package, or `None` for default.
    pub toolchain: Option<String>,
    /// Whether to compile the package with the default features.
    pub default_features: bool,
    /// Features to compile the package with.
    pub features: BTreeSet<String>,
    /// Equivalent to `build_profile = Some("dev")` but binds stronger
    pub debug: Option<bool>,
    /// The build profile (`test` or `bench` or one from *~/.cargo/config.toml* `[profile.gaming]`); CANNOT be `dev` (`debug =
    /// Some(true)`) or `release` (`debug = build_profile = None`)
    pub build_profile: Option<Cow<'static, str>>,
    /// Whether to install pre-release versions.
    pub install_prereleases: Option<bool>,
    /// Whether to enforce Cargo.lock versions.
    pub enforce_lock: Option<bool>,
    /// Whether to install only the pre-configured binaries.
    pub respect_binaries: Option<bool>,
    /// Versions to constrain to.
    pub target_version: Option<VersionReq>,
    /// Environment variables to alter for cargo. `None` to remove.
    pub environment: Option<BTreeMap<String, EnvironmentOverride>>,
    /// Read in from `.crates2.json`, shouldn't be saved
    #[serde(skip)]
    pub from_transient: bool,
}
impl PartialEq for PackageConfig {
    fn eq(&self, other: &Self) -> bool {
        self.toolchain /************/ == other.toolchain && // !
        self.default_features /*****/ == other.default_features && // !
        self.features /*************/ == other.features && // !
        self.debug /****************/ == other.debug && // !
        self.build_profile /********/ == other.build_profile && // !
        self.install_prereleases /**/ == other.install_prereleases && // !
        self.enforce_lock /*********/ == other.enforce_lock && // !
        self.respect_binaries /*****/ == other.respect_binaries && // !
        self.target_version /*******/ == other.target_version && // !
        self.environment /**********/ == other.environment
        // No from_transient
    }
}


impl PackageConfig {
    /// Create a package config based on the default settings and modified according to the specified operations.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate cargo_update;
    /// # extern crate semver;
    /// # fn main() {
    /// # use cargo_update::ops::{EnvironmentOverride, ConfigOperation, PackageConfig};
    /// # use std::collections::BTreeSet;
    /// # use std::collections::BTreeMap;
    /// # use semver::VersionReq;
    /// # use std::str::FromStr;
    /// assert_eq!(PackageConfig::from(&[ConfigOperation::SetToolchain("nightly".to_string()),
    ///                                  ConfigOperation::DefaultFeatures(false),
    ///                                  ConfigOperation::AddFeature("rustc-serialize".to_string()),
    ///                                  ConfigOperation::SetBuildProfile("dev".into()),
    ///                                  ConfigOperation::SetInstallPrereleases(false),
    ///                                  ConfigOperation::SetEnforceLock(true),
    ///                                  ConfigOperation::SetRespectBinaries(true),
    ///                                  ConfigOperation::SetTargetVersion(VersionReq::from_str(">=0.1").unwrap()),
    ///                                  ConfigOperation::SetEnvironment("RUSTC_WRAPPER".to_string(), "sccache".to_string()),
    ///                                  ConfigOperation::ClearEnvironment("CC".to_string())]),
    ///            PackageConfig {
    ///                toolchain: Some("nightly".to_string()),
    ///                default_features: false,
    ///                features: {
    ///                    let mut feats = BTreeSet::new();
    ///                    feats.insert("rustc-serialize".to_string());
    ///                    feats
    ///                },
    ///                debug: Some(true),
    ///                build_profile: None,
    ///                install_prereleases: Some(false),
    ///                enforce_lock: Some(true),
    ///                respect_binaries: Some(true),
    ///                target_version: Some(VersionReq::from_str(">=0.1").unwrap()),
    ///                environment: Some({
    ///                    let mut vars = BTreeMap::new();
    ///                    vars.insert("RUSTC_WRAPPER".to_string(), EnvironmentOverride(Some("sccache".to_string())));
    ///                    vars.insert("CC".to_string(), EnvironmentOverride(None));
    ///                    vars
    ///                }),
    ///                from_transient: false,
    ///            });
    /// # }
    /// ```
    pub fn from<'o, O: IntoIterator<Item = &'o ConfigOperation>>(ops: O) -> PackageConfig {
        let mut def = PackageConfig::default();
        def.execute_operations(ops);
        def
    }

    /// Generate cargo arguments from this configuration.
    ///
    /// Executable names are stripped of their trailing `".exe"`, if any.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use cargo_update::ops::PackageConfig;
    /// # use std::collections::BTreeMap;
    /// # use std::process::Command;
    /// # let name = "cargo-update".to_string();
    /// # let mut configuration = BTreeMap::new();
    /// # configuration.insert(name.clone(), PackageConfig::from(&[]));
    /// let cmd = Command::new("cargo")
    ///               .args(configuration.get(&name).unwrap().cargo_args(&["racer"]).iter().map(AsRef::as_ref))
    ///               .arg(&name)
    /// // Process the command further -- run it, for example.
    /// # .status().unwrap();
    /// # let _ = cmd;
    /// ```
    pub fn cargo_args<S: AsRef<str>, I: IntoIterator<Item = S>>(&self, executables: I) -> Vec<Cow<'static, str>> {
        let mut res = vec![];
        if let Some(ref t) = self.toolchain {
            res.push(format!("+{}", t).into());
        }
        res.push("install".into());
        res.push("-f".into());
        if !self.default_features {
            res.push("--no-default-features".into());
        }
        if !self.features.is_empty() {
            res.push("--features".into());
            let mut a = String::new();
            for f in &self.features {
                write!(a, "{} ", f).unwrap();
            }
            res.push(a.into());
        }
        if let Some(true) = self.enforce_lock {
            res.push("--locked".into());
        }
        if let Some(true) = self.respect_binaries {
            for x in executables {
                let x = x.as_ref();

                res.push("--bin".into());
                res.push(x.strip_suffix(".exe").unwrap_or(x).to_string().into());
            }
        }
        if let Some(true) = self.debug {
            res.push("--debug".into());
        } else if let Some(prof) = self.build_profile.as_ref() {
            res.push("--profile".into());
            res.push(prof.clone());
        }
        res
    }

    /// Apply transformations from `self.environment` to `cmd`.
    pub fn environmentalise<'c>(&self, cmd: &'c mut Command) -> &'c mut Command {
        if let Some(env) = self.environment.as_ref() {
            for (var, val) in env {
                match val {
                    EnvironmentOverride(Some(val)) => cmd.env(var, val),
                    EnvironmentOverride(None) => cmd.env_remove(var),
                };
            }
        }
        cmd
    }

    /// Modify `self` according to the specified set of operations.
    ///
    /// If this config was transient (read in from `.crates2.json`), it is made real and will be saved.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate cargo_update;
    /// # extern crate semver;
    /// # fn main() {
    /// # use cargo_update::ops::{ConfigOperation, PackageConfig};
    /// # use std::collections::BTreeSet;
    /// # use semver::VersionReq;
    /// # use std::str::FromStr;
    /// let mut cfg = PackageConfig {
    ///     toolchain: Some("nightly".to_string()),
    ///     default_features: false,
    ///     features: {
    ///         let mut feats = BTreeSet::new();
    ///         feats.insert("rustc-serialize".to_string());
    ///         feats
    ///     },
    ///     debug: None,
    ///     build_profile: None,
    ///     install_prereleases: None,
    ///     enforce_lock: None,
    ///     respect_binaries: None,
    ///     target_version: Some(VersionReq::from_str(">=0.1").unwrap()),
    ///     environment: None,
    ///     from_transient: false,
    /// };
    /// cfg.execute_operations(&[ConfigOperation::RemoveToolchain,
    ///                          ConfigOperation::AddFeature("serde".to_string()),
    ///                          ConfigOperation::RemoveFeature("rustc-serialize".to_string()),
    ///                          ConfigOperation::SetBuildProfile("dev".into()),
    ///                          ConfigOperation::RemoveTargetVersion]);
    /// assert_eq!(cfg,
    ///            PackageConfig {
    ///                toolchain: None,
    ///                default_features: false,
    ///                features: {
    ///                    let mut feats = BTreeSet::new();
    ///                    feats.insert("serde".to_string());
    ///                    feats
    ///                },
    ///                debug: Some(true),
    ///                build_profile: None,
    ///                install_prereleases: None,
    ///                enforce_lock: None,
    ///                respect_binaries: None,
    ///                target_version: None,
    ///                environment: None,
    ///                from_transient: false,
    ///            });
    /// # }
    /// ```
    pub fn execute_operations<'o, O: IntoIterator<Item = &'o ConfigOperation>>(&mut self, ops: O) {
        self.from_transient = false;
        for op in ops {
            self.execute_operation(op)
        }
    }

    fn execute_operation(&mut self, op: &ConfigOperation) {
        match op {
            ConfigOperation::SetToolchain(ref tchn) => self.toolchain = Some(tchn.clone()),
            ConfigOperation::RemoveToolchain => self.toolchain = None,
            ConfigOperation::DefaultFeatures(f) => self.default_features = *f,
            ConfigOperation::AddFeature(ref feat) => {
                self.features.insert(feat.clone());
            }
            ConfigOperation::RemoveFeature(ref feat) => {
                self.features.remove(feat);
            }
            ConfigOperation::SetBuildProfile(d) => {
                self.debug = None;
                self.build_profile = Some(d.clone());
                self.normalise();
            }
            ConfigOperation::SetInstallPrereleases(pr) => self.install_prereleases = Some(*pr),
            ConfigOperation::SetEnforceLock(el) => self.enforce_lock = Some(*el),
            ConfigOperation::SetRespectBinaries(rb) => self.respect_binaries = Some(*rb),
            ConfigOperation::SetTargetVersion(ref vr) => self.target_version = Some(vr.clone()),
            ConfigOperation::RemoveTargetVersion => self.target_version = None,
            ConfigOperation::SetEnvironment(ref var, ref val) => {
                self.environment.get_or_insert(Default::default()).insert(var.clone(), EnvironmentOverride(Some(val.clone())));
            }
            ConfigOperation::ClearEnvironment(ref var) => {
                self.environment.get_or_insert(Default::default()).insert(var.clone(), EnvironmentOverride(None));
            }
            ConfigOperation::InheritEnvironment(ref var) => {
                self.environment.get_or_insert(Default::default()).remove(var);
            }
            ConfigOperation::ResetConfig => *self = Default::default(),
        }
    }

    /// Read a configset from the specified file, or from the given `.cargo2.json`.
    ///
    /// The first file (usually `.install_config.toml`) is used by default for each package;
    /// `.cargo2.json`, if any, is used to backfill existing data from cargo.
    ///
    /// If the specified file doesn't exist an empty configset is returned.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::collections::{BTreeSet, BTreeMap};
    /// # use cargo_update::ops::PackageConfig;
    /// # use std::fs::{self, create_dir_all};
    /// # use std::env::temp_dir;
    /// # use std::path::Path;
    /// # use std::io::Write;
    /// # let td = temp_dir().join("cargo_update-doctest").join("PackageConfig-read-0");
    /// # create_dir_all(&td).unwrap();
    /// # let config_file = td.join(".install_config.toml");
    /// fs::write(&config_file, &b"\
    ///    [cargo-update]\n\
    ///    default_features = true\n\
    ///    features = [\"serde\"]\n"[..]).unwrap();
    /// assert_eq!(PackageConfig::read(&config_file, Path::new("/ENOENT")), Ok({
    ///     let mut pkgs = BTreeMap::new();
    ///     pkgs.insert("cargo-update".to_string(), PackageConfig {
    ///         toolchain: None,
    ///         default_features: true,
    ///         features: {
    ///             let mut feats = BTreeSet::new();
    ///             feats.insert("serde".to_string());
    ///             feats
    ///         },
    ///         debug: None,
    ///         build_profile: None,
    ///         install_prereleases: None,
    ///         enforce_lock: None,
    ///         respect_binaries: None,
    ///         target_version: None,
    ///         environment: None,
    ///         from_transient: false,
    ///     });
    ///     pkgs
    /// }));
    /// ```
    pub fn read(p: &Path, cargo2_json: &Path) -> Result<BTreeMap<String, PackageConfig>, (String, i32)> {
        let mut base = match fs::read_to_string(p) {
            Ok(s) => toml::from_str(&s).map_err(|e| (e.to_string(), 2))?,
            Err(e) if e.kind() == IoErrorKind::NotFound => BTreeMap::new(),
            Err(e) => Err((e.to_string(), 1))?,
        };
        // {
        //   "installs": {
        //     "pixelmatch 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)": {
        //       "version_req": null,
        //       "bins": [
        //         "pixelmatch"
        //       ],
        //       "features": [
        //         "build-binary"
        //       ],
        //       "all_features": false,
        //       "no_default_features": false,
        //       "profile": "release",
        //       "target": "x86_64-unknown-linux-gnu",
        //       "rustc": "rustc 1.54.0 (a178d0322 2021-07-26)\nbinary: ..."
        //     },
        if let Ok(cargo2_data) = fs::read(cargo2_json) {
            if let Ok(json::Value::Object(mut cargo2)) = json::from_slice(&cargo2_data[..]) {
                if let Some(json::Value::Object(installs)) = cargo2.remove("installs") {
                    for (k, v) in installs {
                        if let json::Value::Object(v) = v {
                            if let Some((name, _, _)) = super::parse_registry_package_ident(&k).or_else(|| super::parse_git_package_ident(&k)) {
                                if !base.contains_key(name) {
                                    base.insert(name.to_string(), PackageConfig::cargo2_package_config(v));
                                }
                            }
                        }
                    }
                }
            }
        }
        for (_, v) in &mut base {
            v.normalise();
        }
        Ok(base)
    }

    fn normalise(&mut self) {
        if self.debug.unwrap_or(false) && self.build_profile.is_none() {
            self.build_profile = Some("dev".into());
        }

        match self.build_profile.as_deref().unwrap_or("release") {
            "dev" => {
                self.debug = Some(true);
                self.build_profile = None;
            }
            "release" => {
                self.debug = None;
                self.build_profile = None;
            }
            _ => {
                self.debug = None;
                // self.build_profile unchanged
            }
        }
    }

    fn cargo2_package_config(mut blob: json::Map<String, json::Value>) -> PackageConfig {
        let mut ret = PackageConfig::default();
        ret.from_transient = true;

        // Nothing to parse PackageConfig::toolchain from
        if let Some(json::Value::Bool(ndf)) = blob.get("no_default_features") {
            ret.default_features = !ndf;
        }
        if let Some(json::Value::Array(fs)) = blob.remove("features") {
            ret.features = fs.into_iter()
                .filter_map(|f| match f {
                    json::Value::String(s) => Some(s),
                    _ => None,
                })
                .collect();
        }
        // Nothing to parse "all_features" into
        if let Some(json::Value::String(prof)) = blob.remove("profile") {
            ret.build_profile = Some(prof.into());
        }
        // Nothing to parse PackageConfig::install_prereleases from
        // Nothing to parse PackageConfig::enforce_lock from
        // "bins" is kinda like PackageConfig::respect_binaries but no really
        // "version_req" is set by cargo install --version, so we'd lock after the first update if we parsed it like this
        // Nothing to parse PackageConfig::environment from
        ret
    }

    /// Save a configset to the specified file, transient (`.crates2.json`) configs are removed.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::collections::{BTreeSet, BTreeMap};
    /// # use cargo_update::ops::PackageConfig;
    /// # use std::fs::{self, create_dir_all};
    /// # use std::env::temp_dir;
    /// # use std::io::Read;
    /// # let td = temp_dir().join("cargo_update-doctest").join("PackageConfig-write-0");
    /// # create_dir_all(&td).unwrap();
    /// # let config_file = td.join(".install_config.toml");
    /// PackageConfig::write(&{
    ///     let mut pkgs = BTreeMap::new();
    ///     pkgs.insert("cargo-update".to_string(), PackageConfig {
    ///         toolchain: None,
    ///         default_features: true,
    ///         features: {
    ///             let mut feats = BTreeSet::new();
    ///             feats.insert("serde".to_string());
    ///             feats
    ///         },
    ///         debug: None,
    ///         build_profile: None,
    ///         install_prereleases: None,
    ///         enforce_lock: None,
    ///         respect_binaries: None,
    ///         target_version: None,
    ///         environment: None,
    ///         from_transient: false,
    ///     });
    ///     pkgs
    /// }, &config_file).unwrap();
    ///
    /// assert_eq!(&fs::read_to_string(&config_file).unwrap(),
    ///            "[cargo-update]\n\
    ///             default_features = true\n\
    ///             features = [\"serde\"]\n");
    /// ```
    pub fn write(configuration: &BTreeMap<String, PackageConfig>, p: &Path) -> Result<(), (String, i32)> {
        fs::write(p, &toml::to_string(&FilteredPackageConfigMap(configuration)).map_err(|e| (e.to_string(), 2))?).map_err(|e| (e.to_string(), 3))
    }
}

impl Default for PackageConfig {
    fn default() -> PackageConfig {
        PackageConfig {
            toolchain: None,
            default_features: true,
            features: BTreeSet::new(),
            debug: None,
            build_profile: None,
            install_prereleases: None,
            enforce_lock: None,
            respect_binaries: None,
            target_version: None,
            environment: None,
            from_transient: false,
        }
    }
}

struct FilteredPackageConfigMap<'a>(pub &'a BTreeMap<String, PackageConfig>);
impl<'a> Serialize for FilteredPackageConfigMap<'a> {
    #[inline]
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.collect_map(self.0.iter().filter(|(_, v)| !v.from_transient))
    }
}


/// Wrapper that serialises `None` as a boolean.
///
/// serde's default `BTreeMap<String, Option<String>>` implementation simply loses `None` values.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct EnvironmentOverride(pub Option<String>);

impl<'de> Deserialize<'de> for EnvironmentOverride {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        deserializer.deserialize_any(EnvironmentOverrideVisitor)
    }
}

impl Serialize for EnvironmentOverride {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match &self.0 {
            Some(data) => serializer.serialize_str(&data),
            None => serializer.serialize_bool(false),
        }
    }
}

struct EnvironmentOverrideVisitor;

impl<'de> de::Visitor<'de> for EnvironmentOverrideVisitor {
    type Value = EnvironmentOverride;

    fn expecting(&self, formatter: &mut FFormatter) -> FResult {
        write!(formatter, "A string or boolean")
    }

    fn visit_bool<E: de::Error>(self, _: bool) -> Result<Self::Value, E> {
        Ok(EnvironmentOverride(None))
    }

    fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
        Ok(EnvironmentOverride(Some(s.to_string())))
    }
}
