//! serde-based `Cargo.lock` parser/serializer
//!
//! Customized to allow pre/postprocessing to detect and serialize both
//! the V1 vs V2 formats and ensure the end-user is supplied a consistent
//! representation regardless of which version is in use.
//!
//! Parts adapted from upstream Cargo.
//! Cargo is primarily distributed under the terms of both the MIT license and
//! the Apache License (Version 2.0).

use super::{Lockfile, ResolveVersion};
use crate::{
    metadata, Checksum, Dependency, Error, Metadata, Name, Package, Patch, Result, SourceId,
    Version,
};
use serde::{de, ser, Deserialize, Serialize};
use std::{collections::HashMap, collections::HashSet, fmt, fmt::Write, str::FromStr};

impl<'de> Deserialize<'de> for Lockfile {
    fn deserialize<D: de::Deserializer<'de>>(
        deserializer: D,
    ) -> std::result::Result<Self, D::Error> {
        EncodableLockfile::deserialize(deserializer)?
            .try_into()
            .map_err(de::Error::custom)
    }
}

impl Serialize for Lockfile {
    fn serialize<S: ser::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
        EncodableLockfile::from(self).serialize(serializer)
    }
}

/// Serialization-oriented equivalent to [`Lockfile`]
#[derive(Debug, Deserialize, Serialize)]
pub(super) struct EncodableLockfile {
    /// Lockfile version
    pub(super) version: Option<u32>,

    /// Packages in the lockfile
    #[serde(default)]
    pub(super) package: Vec<EncodablePackage>,

    /// Legacy root package (preserved for compatibility)
    pub(super) root: Option<EncodablePackage>,

    /// Metadata fields
    #[serde(default, skip_serializing_if = "Metadata::is_empty")]
    pub(super) metadata: Metadata,

    /// Patch section
    #[serde(default, skip_serializing_if = "Patch::is_empty")]
    pub(super) patch: Patch,
}

impl EncodableLockfile {
    /// Attempt to find a checksum for a package in a V1 lockfile
    pub fn find_checksum(&self, package: &Package) -> Option<Checksum> {
        for (key, value) in &self.metadata {
            if let Ok(dep) = key.checksum_dependency() {
                if dep.name == package.name && dep.version == package.version {
                    return value.checksum().ok();
                }
            }
        }

        None
    }
}

impl TryFrom<EncodableLockfile> for Lockfile {
    type Error = Error;

    fn try_from(raw_lockfile: EncodableLockfile) -> Result<Lockfile> {
        let version = match raw_lockfile.version {
            Some(n) => n.try_into()?,
            None => ResolveVersion::detect(&raw_lockfile.package, &raw_lockfile.metadata)?,
        };

        let mut packages = Vec::with_capacity(raw_lockfile.package.len());

        for raw_package in &raw_lockfile.package {
            packages.push(if version == ResolveVersion::V1 {
                // In the V1 format, all dependencies are fully qualified with
                // their versions, but their checksums are stored in metadata.
                let mut pkg = Package::try_from(raw_package)?;
                pkg.checksum = raw_lockfile.find_checksum(&pkg);
                pkg
            } else {
                // In newer versions, we may need to look up dependency versions
                // from the other packages listed in the lockfile
                raw_package.resolve(&raw_lockfile.package)?
            })
        }

        Ok(Lockfile {
            version,
            packages,
            root: raw_lockfile
                .root
                .as_ref()
                .map(|root| root.try_into())
                .transpose()?,
            metadata: raw_lockfile.metadata,
            patch: raw_lockfile.patch,
        })
    }
}

impl From<&Lockfile> for EncodableLockfile {
    fn from(lockfile: &Lockfile) -> EncodableLockfile {
        let mut packages = Vec::with_capacity(lockfile.packages.len());
        let mut metadata = lockfile.metadata.clone();

        let mut package_to_registries: HashMap<_, HashSet<&SourceId>> = HashMap::new();
        for package in &lockfile.packages {
            if let Some(source) = package.source.as_ref() {
                if source.is_registry() {
                    package_to_registries
                        .entry(&package.name)
                        .or_default()
                        .insert(source);
                }
            }
        }

        for package in &lockfile.packages {
            let mut raw_pkg = EncodablePackage::from(package);
            let checksum_key = metadata::MetadataKey::for_checksum(&Dependency::from(package));

            if lockfile.version == ResolveVersion::V1 {
                // In the V1 format, we need to remove the checksum from
                // packages and add it to metadata
                if let Some(checksum) = raw_pkg.checksum.take() {
                    let value = checksum
                        .to_string()
                        .parse::<metadata::MetadataValue>()
                        .unwrap();
                    metadata.insert(checksum_key, value);
                }
            } else {
                // In newer versions, we need to remove the version/source from
                // unambiguous dependencies, and remove checksums from the
                // metadata table if present
                raw_pkg.v2_deps(&lockfile.packages);
                metadata.remove(&checksum_key);

                // If there's only one registry, Cargo does not encode this information so
                // it should be omitted from encodable packages
                for dep in raw_pkg.dependencies.iter_mut() {
                    if let Some(registries) = package_to_registries.get(&dep.name) {
                        if registries.len() == 1 {
                            dep.source = None;
                        }
                    }
                }
            }

            packages.push(raw_pkg);
        }

        let version = if lockfile.version.is_explicit() {
            Some(lockfile.version.into())
        } else {
            None
        };

        EncodableLockfile {
            version,
            package: packages,
            root: lockfile.root.as_ref().map(|root| root.into()),
            metadata,
            patch: lockfile.patch.clone(),
        }
    }
}

impl ToString for EncodableLockfile {
    /// Adapted from `serialize_resolve` in upstream Cargo:
    /// <https://github.com/rust-lang/cargo/blob/0c70319/src/cargo/ops/lockfile.rs#L103-L174>
    fn to_string(&self) -> String {
        let toml = toml::Value::try_from(self).unwrap();
        let mut out = String::new();

        // At the start of the file we notify the reader that the file is generated.
        // Specifically Phabricator ignores files containing "@generated", so we use that.
        let marker_line = "# This file is automatically @generated by Cargo.";
        let extra_line = "# It is not intended for manual editing.";
        out.push_str(marker_line);
        out.push('\n');
        out.push_str(extra_line);
        out.push('\n');

        if let Some(value) = toml.get("version") {
            if let Some(version) = value.as_integer() {
                if version >= 3 {
                    writeln!(out, "version = {}", version).unwrap();
                }
            }
        }

        out.push('\n');

        let deps = toml["package"].as_array().unwrap();
        for dep in deps {
            let dep = dep.as_table().unwrap();

            out.push_str("[[package]]\n");
            emit_package(dep, &mut out);
        }

        if let Some(patch) = toml.get("patch") {
            let list = patch["unused"].as_array().unwrap();
            for entry in list {
                out.push_str("[[patch.unused]]\n");
                emit_package(entry.as_table().unwrap(), &mut out);
                out.push('\n');
            }
        }

        if let Some(meta) = toml.get("metadata") {
            out.push_str("[metadata]\n");
            out.push_str(&meta.to_string());
        }

        // Trim redundant newlines
        if out.ends_with("\n\n") {
            out.pop();
        }

        out
    }
}

/// Emit a single package from a lockfile.
///
/// This method is adapted from the same-named method in upstream Cargo:
/// <https://github.com/rust-lang/cargo/blob/0c70319/src/cargo/ops/lockfile.rs#L194-L221>
fn emit_package(dep: &toml::value::Table, out: &mut String) {
    writeln!(out, "name = {}", &dep["name"]).unwrap();
    writeln!(out, "version = {}", &dep["version"]).unwrap();

    if dep.contains_key("source") {
        writeln!(out, "source = {}", &dep["source"]).unwrap();
    }
    if dep.contains_key("checksum") {
        writeln!(out, "checksum = {}", &dep["checksum"]).unwrap();
    }

    if let Some(s) = dep.get("dependencies") {
        let slice = s.as_array().unwrap();

        if !slice.is_empty() {
            out.push_str("dependencies = [\n");

            for child in slice.iter() {
                writeln!(out, " {},", child).unwrap();
            }

            out.push_str("]\n");
        }
    } else if dep.contains_key("replace") {
        writeln!(out, "replace = {}", &dep["replace"]).unwrap();
    }

    out.push('\n');
}

/// Serialization-oriented equivalent to [`Package`]
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct EncodablePackage {
    /// Package name
    pub(super) name: Name,

    /// Package version
    pub(super) version: Version,

    /// Source of a package
    pub(super) source: Option<SourceId>,

    /// Package checksum
    pub(super) checksum: Option<Checksum>,

    /// Package dependencies
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub(super) dependencies: Vec<EncodableDependency>,

    /// Replace directive
    pub(super) replace: Option<EncodableDependency>,
}

impl EncodablePackage {
    /// Resolve all of the dependencies of a package, which in the V2 format
    /// may be abbreviated to prevent merge conflicts
    fn resolve(&self, packages: &[EncodablePackage]) -> Result<Package> {
        let mut dependencies = Vec::with_capacity(self.dependencies.len());

        for dep in &self.dependencies {
            dependencies.push(dep.resolve(packages)?);
        }

        Ok(Package {
            name: self.name.clone(),
            version: self.version.clone(),
            source: self.source.clone(),
            checksum: self.checksum.clone(),
            dependencies,
            replace: self
                .replace
                .as_ref()
                .map(|rep| rep.try_into())
                .transpose()?,
        })
    }

    /// Prepare `ResolveVersion::V2` dependencies by removing ones which are unambiguous
    fn v2_deps(&mut self, packages: &[Package]) {
        for dependency in &mut self.dependencies {
            dependency.v2(packages);
        }
    }
}

/// Note: this only works for `ResolveVersion::V1` dependencies.
impl TryFrom<&EncodablePackage> for Package {
    type Error = Error;

    fn try_from(raw_package: &EncodablePackage) -> Result<Package> {
        raw_package.resolve(&[])
    }
}

impl From<&Package> for EncodablePackage {
    fn from(package: &Package) -> EncodablePackage {
        EncodablePackage {
            name: package.name.clone(),
            version: package.version.clone(),
            source: package.source.clone(),
            checksum: package.checksum.clone(),
            dependencies: package
                .dependencies
                .iter()
                .map(|dep| dep.into())
                .collect::<Vec<_>>(),
            replace: package.replace.as_ref().map(|rep| rep.into()),
        }
    }
}

/// Package dependencies
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub(crate) struct EncodableDependency {
    /// Name of the dependency
    pub(super) name: Name,

    /// Version of the dependency
    pub(super) version: Option<Version>,

    /// Source for the dependency
    pub(super) source: Option<SourceId>,
}

impl EncodableDependency {
    /// Resolve this dependency, which in the V2 format may be abbreviated to
    /// prevent merge conflicts
    pub fn resolve(&self, packages: &[EncodablePackage]) -> Result<Dependency> {
        for pkg in packages {
            if pkg.name == self.name
                && (self.version.is_none() || self.version.as_ref() == Some(&pkg.version))
                && self.source.is_none()
            {
                return Ok(Dependency {
                    name: pkg.name.clone(),
                    version: pkg.version.clone(),
                    source: pkg.source.clone(),
                });
            }
        }

        let version = self
            .version
            .clone()
            .ok_or_else(|| Error::Parse(format!("couldn't resolve dependency: {}", self.name)))?;

        Ok(Dependency {
            name: self.name.clone(),
            version,
            source: self.source.clone(),
        })
    }

    /// Prepare `ResolveVersion::V2` dependencies by removing ones which are unambiguous
    pub fn v2(&mut self, packages: &[Package]) {
        let mut matching = vec![];

        for package in packages {
            if package.name == self.name {
                matching.push(package);
            }
        }

        // TODO(tarcieri): better handle other cases?
        if matching.len() == 1 {
            self.version = None;
            self.source = None;
        }
    }
}

impl FromStr for EncodableDependency {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let mut parts = s.split_whitespace();

        let name = parts
            .next()
            .ok_or_else(|| Error::Parse("empty dependency string".to_owned()))?
            .parse()?;

        let version = parts.next().map(FromStr::from_str).transpose()?;

        let source = parts
            .next()
            .map(|s| {
                if s.len() < 2 || !s.starts_with('(') || !s.ends_with(')') {
                    Err(Error::Parse(format!(
                        "malformed source in dependency: {}",
                        s
                    )))
                } else {
                    s[1..(s.len() - 1)].parse()
                }
            })
            .transpose()?;

        if parts.next().is_some() {
            return Err(Error::Parse(format!("malformed dependency: {}", s)));
        }

        Ok(Self {
            name,
            version,
            source,
        })
    }
}

impl fmt::Display for EncodableDependency {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", &self.name)?;

        if let Some(version) = &self.version {
            write!(f, " {}", version)?;
        }

        if let Some(source) = &self.source {
            write!(f, " ({})", source)?;
        }

        Ok(())
    }
}

/// Note: this only works for `ResolveVersion::V1` dependencies.
impl TryFrom<&EncodableDependency> for Dependency {
    type Error = Error;

    fn try_from(raw_dependency: &EncodableDependency) -> Result<Dependency> {
        raw_dependency.resolve(&[])
    }
}

impl From<&Dependency> for EncodableDependency {
    fn from(package: &Dependency) -> EncodableDependency {
        EncodableDependency {
            name: package.name.clone(),
            version: Some(package.version.clone()),
            source: package.source.clone(),
        }
    }
}

impl<'de> Deserialize<'de> for EncodableDependency {
    fn deserialize<D: de::Deserializer<'de>>(
        deserializer: D,
    ) -> std::result::Result<Self, D::Error> {
        String::deserialize(deserializer)?
            .parse()
            .map_err(de::Error::custom)
    }
}

impl Serialize for EncodableDependency {
    fn serialize<S: ser::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
        self.to_string().serialize(serializer)
    }
}
