use cargo_platform::Platform;
use log::trace;
use semver::VersionReq;
use serde::ser;
use serde::Serialize;
use std::borrow::Cow;
use std::fmt;
use std::path::PathBuf;
use std::rc::Rc;

use crate::core::compiler::{CompileKind, CompileTarget};
use crate::core::{PackageId, SourceId, Summary};
use crate::util::errors::CargoResult;
use crate::util::interning::InternedString;
use crate::util::toml::StringOrVec;
use crate::util::OptVersionReq;

/// Information about a dependency requested by a Cargo manifest.
/// Cheap to copy.
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct Dependency {
    inner: Rc<Inner>,
}

/// The data underlying a `Dependency`.
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
struct Inner {
    name: InternedString,
    source_id: SourceId,
    /// Source ID for the registry as specified in the manifest.
    ///
    /// This will be None if it is not specified (crates.io dependency).
    /// This is different from `source_id` for example when both a `path` and
    /// `registry` is specified. Or in the case of a crates.io dependency,
    /// `source_id` will be crates.io and this will be None.
    registry_id: Option<SourceId>,
    req: OptVersionReq,
    specified_req: bool,
    kind: DepKind,
    only_match_name: bool,
    explicit_name_in_toml: Option<InternedString>,

    optional: bool,
    public: bool,
    default_features: bool,
    features: Vec<InternedString>,
    // The presence of this information turns a dependency into an artifact dependency.
    artifact: Option<Artifact>,

    // This dependency should be used only for this platform.
    // `None` means *all platforms*.
    platform: Option<Platform>,
}

#[derive(Serialize)]
struct SerializedDependency<'a> {
    name: &'a str,
    source: SourceId,
    req: String,
    kind: DepKind,
    rename: Option<&'a str>,

    optional: bool,
    uses_default_features: bool,
    features: &'a [InternedString],
    #[serde(skip_serializing_if = "Option::is_none")]
    artifact: Option<&'a Artifact>,
    target: Option<&'a Platform>,
    /// The registry URL this dependency is from.
    /// If None, then it comes from the default registry (crates.io).
    registry: Option<&'a str>,

    /// The file system path for a local path dependency.
    #[serde(skip_serializing_if = "Option::is_none")]
    path: Option<PathBuf>,
}

impl ser::Serialize for Dependency {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        let registry_id = self.registry_id();
        SerializedDependency {
            name: &*self.package_name(),
            source: self.source_id(),
            req: self.version_req().to_string(),
            kind: self.kind(),
            optional: self.is_optional(),
            uses_default_features: self.uses_default_features(),
            features: self.features(),
            target: self.platform(),
            rename: self.explicit_name_in_toml().map(|s| s.as_str()),
            registry: registry_id.as_ref().map(|sid| sid.url().as_str()),
            path: self.source_id().local_path(),
            artifact: self.artifact(),
        }
        .serialize(s)
    }
}

#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Debug, Copy)]
pub enum DepKind {
    Normal,
    Development,
    Build,
}

impl DepKind {
    pub fn kind_table(&self) -> &'static str {
        match self {
            DepKind::Normal => "dependencies",
            DepKind::Development => "dev-dependencies",
            DepKind::Build => "build-dependencies",
        }
    }
}

impl ser::Serialize for DepKind {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        match *self {
            DepKind::Normal => None,
            DepKind::Development => Some("dev"),
            DepKind::Build => Some("build"),
        }
        .serialize(s)
    }
}

impl Dependency {
    /// Attempt to create a `Dependency` from an entry in the manifest.
    pub fn parse(
        name: impl Into<InternedString>,
        version: Option<&str>,
        source_id: SourceId,
    ) -> CargoResult<Dependency> {
        let name = name.into();
        let (specified_req, version_req) = match version {
            Some(v) => match VersionReq::parse(v) {
                Ok(req) => (true, OptVersionReq::Req(req)),
                Err(err) => {
                    return Err(anyhow::Error::new(err).context(format!(
                        "failed to parse the version requirement `{}` for dependency `{}`",
                        v, name,
                    )))
                }
            },
            None => (false, OptVersionReq::Any),
        };

        let mut ret = Dependency::new_override(name, source_id);
        {
            let ptr = Rc::make_mut(&mut ret.inner);
            ptr.only_match_name = false;
            ptr.req = version_req;
            ptr.specified_req = specified_req;
        }
        Ok(ret)
    }

    pub fn new_override(name: InternedString, source_id: SourceId) -> Dependency {
        assert!(!name.is_empty());
        Dependency {
            inner: Rc::new(Inner {
                name,
                source_id,
                registry_id: None,
                req: OptVersionReq::Any,
                kind: DepKind::Normal,
                only_match_name: true,
                optional: false,
                public: false,
                features: Vec::new(),
                default_features: true,
                specified_req: false,
                platform: None,
                explicit_name_in_toml: None,
                artifact: None,
            }),
        }
    }

    pub fn version_req(&self) -> &OptVersionReq {
        &self.inner.req
    }

    /// This is the name of this `Dependency` as listed in `Cargo.toml`.
    ///
    /// Or in other words, this is what shows up in the `[dependencies]` section
    /// on the left hand side. This is *not* the name of the package that's
    /// being depended on as the dependency can be renamed. For that use
    /// `package_name` below.
    ///
    /// Both of the dependencies below return `foo` for `name_in_toml`:
    ///
    /// ```toml
    /// [dependencies]
    /// foo = "0.1"
    /// ```
    ///
    /// and ...
    ///
    /// ```toml
    /// [dependencies]
    /// foo = { version = "0.1", package = 'bar' }
    /// ```
    pub fn name_in_toml(&self) -> InternedString {
        self.explicit_name_in_toml().unwrap_or(self.inner.name)
    }

    /// The name of the package that this `Dependency` depends on.
    ///
    /// Usually this is what's written on the left hand side of a dependencies
    /// section, but it can also be renamed via the `package` key.
    ///
    /// Both of the dependencies below return `foo` for `package_name`:
    ///
    /// ```toml
    /// [dependencies]
    /// foo = "0.1"
    /// ```
    ///
    /// and ...
    ///
    /// ```toml
    /// [dependencies]
    /// bar = { version = "0.1", package = 'foo' }
    /// ```
    pub fn package_name(&self) -> InternedString {
        self.inner.name
    }

    pub fn source_id(&self) -> SourceId {
        self.inner.source_id
    }

    pub fn registry_id(&self) -> Option<SourceId> {
        self.inner.registry_id
    }

    pub fn set_registry_id(&mut self, registry_id: SourceId) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).registry_id = Some(registry_id);
        self
    }

    pub fn kind(&self) -> DepKind {
        self.inner.kind
    }

    pub fn is_public(&self) -> bool {
        self.inner.public
    }

    /// Sets whether the dependency is public.
    pub fn set_public(&mut self, public: bool) -> &mut Dependency {
        if public {
            // Setting 'public' only makes sense for normal dependencies
            assert_eq!(self.kind(), DepKind::Normal);
        }
        Rc::make_mut(&mut self.inner).public = public;
        self
    }

    pub fn specified_req(&self) -> bool {
        self.inner.specified_req
    }

    /// If none, this dependencies must be built for all platforms.
    /// If some, it must only be built for the specified platform.
    pub fn platform(&self) -> Option<&Platform> {
        self.inner.platform.as_ref()
    }

    /// The renamed name of this dependency, if any.
    ///
    /// If the `package` key is used in `Cargo.toml` then this returns the same
    /// value as `name_in_toml`.
    pub fn explicit_name_in_toml(&self) -> Option<InternedString> {
        self.inner.explicit_name_in_toml
    }

    pub fn set_kind(&mut self, kind: DepKind) -> &mut Dependency {
        if self.is_public() {
            // Setting 'public' only makes sense for normal dependencies
            assert_eq!(kind, DepKind::Normal);
        }
        Rc::make_mut(&mut self.inner).kind = kind;
        self
    }

    /// Sets the list of features requested for the package.
    pub fn set_features(
        &mut self,
        features: impl IntoIterator<Item = impl Into<InternedString>>,
    ) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).features = features.into_iter().map(|s| s.into()).collect();
        self
    }

    /// Sets whether the dependency requests default features of the package.
    pub fn set_default_features(&mut self, default_features: bool) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).default_features = default_features;
        self
    }

    /// Sets whether the dependency is optional.
    pub fn set_optional(&mut self, optional: bool) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).optional = optional;
        self
    }

    /// Sets the source ID for this dependency.
    pub fn set_source_id(&mut self, id: SourceId) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).source_id = id;
        self
    }

    /// Sets the version requirement for this dependency.
    pub fn set_version_req(&mut self, req: VersionReq) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).req = OptVersionReq::Req(req);
        self
    }

    pub fn set_platform(&mut self, platform: Option<Platform>) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).platform = platform;
        self
    }

    pub fn set_explicit_name_in_toml(
        &mut self,
        name: impl Into<InternedString>,
    ) -> &mut Dependency {
        Rc::make_mut(&mut self.inner).explicit_name_in_toml = Some(name.into());
        self
    }

    /// Locks this dependency to depending on the specified package ID.
    pub fn lock_to(&mut self, id: PackageId) -> &mut Dependency {
        assert_eq!(self.inner.source_id, id.source_id());
        trace!(
            "locking dep from `{}` with `{}` at {} to {}",
            self.package_name(),
            self.version_req(),
            self.source_id(),
            id
        );
        let me = Rc::make_mut(&mut self.inner);
        me.req.lock_to(id.version());

        // Only update the `precise` of this source to preserve other
        // information about dependency's source which may not otherwise be
        // tested during equality/hashing.
        me.source_id = me
            .source_id
            .with_precise(id.source_id().precise().map(|s| s.to_string()));
        self
    }

    /// Locks this dependency to a specified version.
    ///
    /// Mainly used in dependency patching like `[patch]` or `[replace]`, which
    /// doesn't need to lock the entire dependency to a specific [`PackageId`].
    pub fn lock_version(&mut self, version: &semver::Version) -> &mut Dependency {
        let me = Rc::make_mut(&mut self.inner);
        me.req.lock_to(version);
        self
    }

    /// Returns `true` if this is a "locked" dependency. Basically a locked
    /// dependency has an exact version req, but not vice versa.
    pub fn is_locked(&self) -> bool {
        self.inner.req.is_locked()
    }

    /// Returns `false` if the dependency is only used to build the local package.
    pub fn is_transitive(&self) -> bool {
        match self.inner.kind {
            DepKind::Normal | DepKind::Build => true,
            DepKind::Development => false,
        }
    }

    pub fn is_build(&self) -> bool {
        matches!(self.inner.kind, DepKind::Build)
    }

    pub fn is_optional(&self) -> bool {
        self.inner.optional
    }

    /// Returns `true` if the default features of the dependency are requested.
    pub fn uses_default_features(&self) -> bool {
        self.inner.default_features
    }
    /// Returns the list of features that are requested by the dependency.
    pub fn features(&self) -> &[InternedString] {
        &self.inner.features
    }

    /// Returns `true` if the package (`sum`) can fulfill this dependency request.
    pub fn matches(&self, sum: &Summary) -> bool {
        self.matches_id(sum.package_id())
    }

    /// Returns `true` if the package (`id`) can fulfill this dependency request.
    pub fn matches_ignoring_source(&self, id: PackageId) -> bool {
        self.package_name() == id.name() && self.version_req().matches(id.version())
    }

    /// Returns `true` if the package (`id`) can fulfill this dependency request.
    pub fn matches_id(&self, id: PackageId) -> bool {
        self.inner.name == id.name()
            && (self.inner.only_match_name
                || (self.inner.req.matches(id.version()) && self.inner.source_id == id.source_id()))
    }

    pub fn map_source(mut self, to_replace: SourceId, replace_with: SourceId) -> Dependency {
        if self.source_id() == to_replace {
            self.set_source_id(replace_with);
        }
        self
    }

    pub(crate) fn set_artifact(&mut self, artifact: Artifact) {
        Rc::make_mut(&mut self.inner).artifact = Some(artifact);
    }

    pub(crate) fn artifact(&self) -> Option<&Artifact> {
        self.inner.artifact.as_ref()
    }

    /// Dependencies are potential rust libs if they are not artifacts or they are an
    /// artifact which allows to be seen as library.
    /// Previously, every dependency was potentially seen as library.
    pub(crate) fn maybe_lib(&self) -> bool {
        self.artifact().map(|a| a.is_lib).unwrap_or(true)
    }
}

/// The presence of an artifact turns an ordinary dependency into an Artifact dependency.
/// As such, it will build one or more different artifacts of possibly various kinds
/// for making them available at build time for rustc invocations or runtime
/// for build scripts.
///
/// This information represents a requirement in the package this dependency refers to.
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct Artifact {
    inner: Rc<Vec<ArtifactKind>>,
    is_lib: bool,
    target: Option<ArtifactTarget>,
}

#[derive(Serialize)]
pub struct SerializedArtifact<'a> {
    kinds: &'a [ArtifactKind],
    lib: bool,
    target: Option<&'a str>,
}

impl ser::Serialize for Artifact {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        SerializedArtifact {
            kinds: self.kinds(),
            lib: self.is_lib,
            target: self.target.as_ref().map(|t| match t {
                ArtifactTarget::BuildDependencyAssumeTarget => "target",
                ArtifactTarget::Force(target) => target.rustc_target().as_str(),
            }),
        }
        .serialize(s)
    }
}

impl Artifact {
    pub(crate) fn parse(
        artifacts: &StringOrVec,
        is_lib: bool,
        target: Option<&str>,
    ) -> CargoResult<Self> {
        let kinds = ArtifactKind::validate(
            artifacts
                .iter()
                .map(|s| ArtifactKind::parse(s))
                .collect::<Result<Vec<_>, _>>()?,
        )?;
        Ok(Artifact {
            inner: Rc::new(kinds),
            is_lib,
            target: target.map(ArtifactTarget::parse).transpose()?,
        })
    }

    pub(crate) fn kinds(&self) -> &[ArtifactKind] {
        &self.inner
    }

    pub(crate) fn is_lib(&self) -> bool {
        self.is_lib
    }

    pub(crate) fn target(&self) -> Option<ArtifactTarget> {
        self.target
    }
}

#[derive(PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd, Debug)]
pub enum ArtifactTarget {
    /// Only applicable to build-dependencies, causing them to be built
    /// for the given target (i.e. via `--target <triple>`) instead of for the host.
    /// Has no effect on non-build dependencies.
    BuildDependencyAssumeTarget,
    /// The name of the platform triple, like `x86_64-apple-darwin`, that this
    /// artifact will always be built for, no matter if it is a build,
    /// normal or dev dependency.
    Force(CompileTarget),
}

impl ArtifactTarget {
    pub fn parse(target: &str) -> CargoResult<ArtifactTarget> {
        Ok(match target {
            "target" => ArtifactTarget::BuildDependencyAssumeTarget,
            name => ArtifactTarget::Force(CompileTarget::new(name)?),
        })
    }

    pub fn to_compile_kind(&self) -> Option<CompileKind> {
        self.to_compile_target().map(CompileKind::Target)
    }

    pub fn to_compile_target(&self) -> Option<CompileTarget> {
        match self {
            ArtifactTarget::BuildDependencyAssumeTarget => None,
            ArtifactTarget::Force(target) => Some(*target),
        }
    }
    pub(crate) fn to_resolved_compile_kind(
        &self,
        root_unit_compile_kind: CompileKind,
    ) -> CompileKind {
        match self {
            ArtifactTarget::Force(target) => CompileKind::Target(*target),
            ArtifactTarget::BuildDependencyAssumeTarget => root_unit_compile_kind,
        }
    }

    pub(crate) fn to_resolved_compile_target(
        &self,
        root_unit_compile_kind: CompileKind,
    ) -> Option<CompileTarget> {
        match self.to_resolved_compile_kind(root_unit_compile_kind) {
            CompileKind::Host => None,
            CompileKind::Target(target) => Some(target),
        }
    }
}

#[derive(PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd, Debug)]
pub enum ArtifactKind {
    /// We represent all binaries in this dependency
    AllBinaries,
    /// We represent a single binary
    SelectedBinary(InternedString),
    Cdylib,
    Staticlib,
}

impl ser::Serialize for ArtifactKind {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        let out: Cow<'_, str> = match *self {
            ArtifactKind::SelectedBinary(name) => format!("bin:{}", name.as_str()).into(),
            _ => self.crate_type().into(),
        };
        out.serialize(s)
    }
}

impl fmt::Display for ArtifactKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            ArtifactKind::SelectedBinary(bin_name) => return write!(f, "bin:{bin_name}"),
            _ => self.crate_type(),
        })
    }
}

impl ArtifactKind {
    /// Returns a string of crate type of the artifact being built.
    ///
    /// Note that the name of `SelectedBinary` would be dropped and displayed as `bin`.
    pub fn crate_type(&self) -> &'static str {
        match self {
            ArtifactKind::AllBinaries | ArtifactKind::SelectedBinary(_) => "bin",
            ArtifactKind::Cdylib => "cdylib",
            ArtifactKind::Staticlib => "staticlib",
        }
    }

    fn parse(kind: &str) -> CargoResult<Self> {
        Ok(match kind {
            "bin" => ArtifactKind::AllBinaries,
            "cdylib" => ArtifactKind::Cdylib,
            "staticlib" => ArtifactKind::Staticlib,
            _ => {
                return kind
                    .strip_prefix("bin:")
                    .map(|bin_name| ArtifactKind::SelectedBinary(InternedString::new(bin_name)))
                    .ok_or_else(|| anyhow::anyhow!("'{}' is not a valid artifact specifier", kind))
            }
        })
    }

    fn validate(kinds: Vec<ArtifactKind>) -> CargoResult<Vec<ArtifactKind>> {
        if kinds.iter().any(|k| matches!(k, ArtifactKind::AllBinaries))
            && kinds
                .iter()
                .any(|k| matches!(k, ArtifactKind::SelectedBinary(_)))
        {
            anyhow::bail!("Cannot specify both 'bin' and 'bin:<name>' binary artifacts, as 'bin' selects all available binaries.");
        }
        let mut kinds_without_dupes = kinds.clone();
        kinds_without_dupes.sort();
        kinds_without_dupes.dedup();
        let num_dupes = kinds.len() - kinds_without_dupes.len();
        if num_dupes != 0 {
            anyhow::bail!(
                "Found {} duplicate binary artifact{}",
                num_dupes,
                (num_dupes > 1).then(|| "s").unwrap_or("")
            );
        }
        Ok(kinds)
    }
}
