// Copyright (c) 2017-2018 ETH Zurich
// Fabian Schuiki <fschuiki@iis.ee.ethz.ch>

//! Package manifest and configuration files.
//!
//! This module provides reading and writing of package manifests and
//! configuration files.

#![deny(missing_docs)]

use std;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt;
use std::fs::File;
use std::hash::Hash;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::str::FromStr;

use glob::glob;
use indexmap::IndexMap;
use miette::{Context as _, IntoDiagnostic as _};
use semver;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_yaml_ng::Value;
#[cfg(unix)]
use subst;

use crate::diagnostic::{Errors, Warnings};
use crate::target::TargetSpec;
use crate::util::*;
use crate::{Error, Result};
use crate::{bail, err};

/// A package manifest.
///
/// This is usually called `Bender.yml` in the root directory of the package.
#[derive(Debug)]
pub struct Manifest {
    /// The package definition.
    pub package: Package,
    /// The dependencies.
    pub dependencies: IndexMap<String, Dependency>,
    /// The source files.
    pub sources: Option<Sources>,
    /// The include directories exported to dependent packages.
    pub export_include_dirs: Vec<(TargetSpec, PathBuf)>,
    /// The plugin binaries.
    pub plugins: IndexMap<String, PathBuf>,
    /// Whether the dependencies of the manifest are frozen.
    pub frozen: bool,
    /// The workspace configuration.
    pub workspace: Workspace,
    /// Vendorized dependencies
    pub vendor_package: Vec<VendorPackage>,
}

impl PrefixPaths for Manifest {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(Manifest {
            dependencies: self.dependencies.prefix_paths(prefix)?,
            sources: self
                .sources
                .map_or(Ok::<Option<Sources>, Error>(None), |src| {
                    Ok(Some(src.prefix_paths(prefix)?))
                })?,
            export_include_dirs: self
                .export_include_dirs
                .into_iter()
                .map(|(trgt, src)| {
                    let prefixed = src.prefix_paths(prefix)?;
                    Ok((trgt, prefixed))
                })
                .collect::<Result<_>>()?,
            plugins: self.plugins.prefix_paths(prefix)?,
            workspace: self.workspace.prefix_paths(prefix)?,
            vendor_package: self.vendor_package.prefix_paths(prefix)?,
            ..self
        })
    }
}

/// A package definition.
///
/// Contains the metadata for an individual package.
#[derive(Serialize, Deserialize, Debug)]
pub struct Package {
    /// The name of the package.
    pub name: String,
    /// A list of package authors. Each author should be of the form `John Doe
    /// <john@doe.com>`.
    pub authors: Option<Vec<String>>,
    /// A short description of the package.
    pub description: Option<String>,
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

/// A dependency.
///
/// The name of the dependency is given implicitly by the key in the hash map
/// that this `Dependency` is accessible through.
#[derive(Clone, Debug)]
pub enum Dependency {
    /// A dependency that can be found in one of the package repositories.
    Version {
        /// The targets for which the dependency should be considered.
        target: TargetSpec,
        /// The version requirement of the package.
        version: semver::VersionReq,
        /// Targets to pass to the dependency
        pass_targets: Vec<PassedTarget>,
    },
    /// A local path dependency. The exact version of the dependency found at
    /// the given path will be used, regardless of any actual versioning
    /// constraints.
    Path {
        /// The targets for which the dependency should be considered.
        target: TargetSpec,
        /// The path to the dependency.
        path: PathBuf,
        /// Targets to pass to the dependency
        pass_targets: Vec<PassedTarget>,
    },
    /// A git dependency specified by a revision.
    GitRevision {
        /// The targets for which the dependency should be considered.
        target: TargetSpec,
        /// The git URL to the package.
        url: String,
        /// The git revision of the package to use. Can be a commit hash, branch, or tag.
        rev: String,
        /// Targets to pass to the dependency
        pass_targets: Vec<PassedTarget>,
    },
    /// A git dependency specified by a version requirement. Works similarly to
    /// the `GitRevision`, but extracts all tags of the form `v.*` from the
    /// repository and matches the version against that.
    GitVersion {
        /// The targets for which the dependency should be considered.
        target: TargetSpec,
        /// The git URL to the package.
        url: String,
        /// The version requirement of the package.
        version: semver::VersionReq,
        /// Targets to pass to the dependency
        pass_targets: Vec<PassedTarget>,
    },
}

impl PrefixPaths for Dependency {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(match self {
            Dependency::Path {
                target,
                path,
                pass_targets,
            } => Dependency::Path {
                target,
                path: path.prefix_paths(prefix)?,
                pass_targets,
            },
            v => v,
        })
    }
}

impl Serialize for Dependency {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use serde::ser::SerializeMap;
        match *self {
            Dependency::Version {
                ref target,
                ref version,
                ref pass_targets,
            } => {
                let mut map = serializer.serialize_map(Some(3))?;
                map.serialize_entry("target", target)?;
                map.serialize_entry("version", &format!("{}", version))?;
                map.serialize_entry("pass_targets", pass_targets)?;
                map.end()
            }
            // format!("{}, {:?}", version, pass_targets).serialize(serializer),
            Dependency::Path {
                ref target,
                ref path,
                ref pass_targets,
            } => {
                let mut map = serializer.serialize_map(Some(3))?;
                map.serialize_entry("target", target)?;
                map.serialize_entry("path", path)?;
                map.serialize_entry("pass_targets", pass_targets)?;
                map.end()
            }

            // path.serialize(serializer),
            Dependency::GitRevision {
                ref target,
                ref url,
                ref rev,
                ref pass_targets,
            } => {
                let mut map = serializer.serialize_map(Some(4))?;
                map.serialize_entry("target", target)?;
                map.serialize_entry("git", url)?;
                map.serialize_entry("rev", rev)?;
                map.serialize_entry("pass_targets", pass_targets)?;
                map.end()
            }
            Dependency::GitVersion {
                ref target,
                ref url,
                ref version,
                ref pass_targets,
            } => {
                let mut map = serializer.serialize_map(Some(4))?;
                map.serialize_entry("target", target)?;
                map.serialize_entry("git", url)?;
                map.serialize_entry("version", &format!("{}", version))?;
                map.serialize_entry("pass_targets", pass_targets)?;
                map.end()
            }
        }
    }
}

/// A group of source files.
#[derive(Debug)]
pub struct Sources {
    /// The targets for which the sources should be considered.
    pub target: TargetSpec,
    /// The directories to search for include files, with their target specs.
    pub include_dirs: Vec<(TargetSpec, PathBuf)>,
    /// The preprocessor definitions, with their target specs.
    pub defines: IndexMap<String, (TargetSpec, Option<String>)>,
    /// The source files.
    pub files: Vec<SourceFile>,
    /// The files in this source will override other files.
    pub override_files: bool,
}

impl PrefixPaths for Sources {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(Sources {
            include_dirs: self
                .include_dirs
                .into_iter()
                .map(|(trgt, dir)| {
                    let prefixed = dir.prefix_paths(prefix)?;
                    Ok((trgt, prefixed))
                })
                .collect::<Result<_>>()?,
            files: self.files.prefix_paths(prefix)?,
            ..self
        })
    }
}

/// A source file.
pub enum SourceFile {
    /// A file.
    File(PathBuf),
    /// A subgroup.
    Group(Box<Sources>),
    /// A systemverilog source.
    SvFile(PathBuf),
    /// A verilog source.
    VerilogFile(PathBuf),
    /// A vhdl source.
    VhdlFile(PathBuf),
}

impl fmt::Debug for SourceFile {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            SourceFile::File(ref path) => {
                fmt::Debug::fmt(path, f)?;
                write!(f, " as <unknown>")
            }
            SourceFile::SvFile(ref path) => {
                fmt::Debug::fmt(path, f)?;
                write!(f, " as SystemVerilog")
            }
            SourceFile::VerilogFile(ref path) => {
                fmt::Debug::fmt(path, f)?;
                write!(f, " as Verilog")
            }
            SourceFile::VhdlFile(ref path) => {
                fmt::Debug::fmt(path, f)?;
                write!(f, " as Vhdl")
            }
            SourceFile::Group(ref srcs) => fmt::Debug::fmt(srcs, f),
        }
    }
}

impl PrefixPaths for SourceFile {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(match self {
            SourceFile::File(path) => SourceFile::File(path.prefix_paths(prefix)?),
            SourceFile::SvFile(path) => SourceFile::SvFile(path.prefix_paths(prefix)?),
            SourceFile::VerilogFile(path) => SourceFile::VerilogFile(path.prefix_paths(prefix)?),
            SourceFile::VhdlFile(path) => SourceFile::VhdlFile(path.prefix_paths(prefix)?),
            SourceFile::Group(group) => SourceFile::Group(Box::new(group.prefix_paths(prefix)?)),
        })
    }
}

/// A workspace configuration.
#[derive(Debug, Default)]
pub struct Workspace {
    /// The directory which will contain working copies of the dependencies.
    pub checkout_dir: Option<PathBuf>,
    /// The locally linked packages.
    pub package_links: IndexMap<PathBuf, String>,
}

impl PrefixPaths for Workspace {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(Workspace {
            checkout_dir: self.checkout_dir.prefix_paths(prefix)?,
            package_links: self
                .package_links
                .into_iter()
                .map(|(k, v)| Ok((k.prefix_paths(prefix)?, v)))
                .collect::<Result<_>>()?,
        })
    }
}

/// Holds the context required to validate a configuration node.
#[derive(Default)]
pub struct ValidationContext<'a> {
    /// The name of the package being validated (for error messages).
    pub package_name: &'a str,
    /// Whether to suppress warnings (for fast-path parsing).
    pub pre_output: bool,
    /// Available remote definitions (for dependency resolution).
    pub remotes: Option<&'a IndexMap<String, RemoteConfig>>,
    /// The default remote to use
    pub default_remote: Option<&'a RemoteConfig>,
}

/// Converts partial configuration into a validated full configuration.
pub trait Validate {
    /// The output type produced by validation.
    type Output;
    /// The error type produced by validation.
    type Error;
    /// Validate self and convert into the non-partial version.
    fn validate(self, vctx: &ValidationContext) -> std::result::Result<Self::Output, Self::Error>;
}

// Implement `Validate` for hash maps of validatable values.
impl<K, V> Validate for IndexMap<K, V>
where
    K: Hash + Eq,
    V: Validate<Error = Error>,
{
    type Output = IndexMap<K, V::Output>;
    type Error = (K, Error);
    fn validate(self, vctx: &ValidationContext) -> std::result::Result<Self::Output, Self::Error> {
        self.into_iter()
            .map(|(k, v)| match v.validate(vctx) {
                Ok(v) => Ok((k, v)),
                Err(e) => Err((k, e)),
            })
            .collect()
    }
}

impl<V> Validate for Vec<V>
where
    V: Validate<Error = Error>,
{
    type Output = Vec<V::Output>;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> std::result::Result<Self::Output, Self::Error> {
        self.into_iter()
            .map(|v| match v.validate(vctx) {
                Ok(v) => Ok(v),
                Err(e) => Err(e),
            })
            .collect()
    }
}

// Implement `Validate` for `StringOrStruct` wrapped validatable values.
impl<T> Validate for StringOrStruct<T>
where
    T: Validate,
{
    type Output = T::Output;
    type Error = T::Error;
    fn validate(self, vctx: &ValidationContext) -> std::result::Result<T::Output, T::Error> {
        self.0.validate(vctx)
    }
}

// Implement `Validate` for `SeqOrStruct` wrapped validatable values.
impl<T, F> Validate for SeqOrStruct<T, F>
where
    T: Validate,
{
    type Output = T::Output;
    type Error = T::Error;
    fn validate(self, vctx: &ValidationContext) -> std::result::Result<T::Output, T::Error> {
        self.0.validate(vctx)
    }
}

// Implement `PrefixPaths` for `StringOrStruct` wrapped prefixable values.
impl<T> PrefixPaths for StringOrStruct<T>
where
    T: PrefixPaths,
{
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(StringOrStruct(self.0.prefix_paths(prefix)?))
    }
}

// Implement `PrefixPaths` for `SeqOrStruct` wrapped prefixable values.
impl<T, F> PrefixPaths for SeqOrStruct<T, F>
where
    T: PrefixPaths,
{
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(SeqOrStruct::new(self.0.prefix_paths(prefix)?))
    }
}

/// A partial manifest.
///
/// Validation turns this into a `Manifest`.
#[derive(Serialize, Deserialize, Debug)]
pub struct PartialManifest {
    /// The package definition.
    pub package: Option<Package>,
    /// The remotes for package repositories.
    pub remotes: Option<IndexMap<String, StringOrStruct<RemoteConfig>>>,
    /// The dependencies.
    pub dependencies: Option<IndexMap<String, StringOrStruct<PartialDependency>>>,
    /// The source files.
    pub sources: Option<SeqOrStruct<PartialSources, PartialSourceFile>>,
    /// The include directories exported to dependent packages.
    pub export_include_dirs: Option<Vec<StringOrStruct<PartialIncludeDir>>>,
    /// The plugin binaries.
    pub plugins: Option<IndexMap<String, String>>,
    /// Whether the dependencies of the manifest are frozen.
    pub frozen: Option<bool>,
    /// The workspace configuration.
    pub workspace: Option<PartialWorkspace>,
    /// External Import dependencies
    pub vendor_package: Option<Vec<PartialVendorPackage>>,
    /// Unknown extra fields
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

impl PartialManifest {
    /// Before fully cloning a git repo locally a manifest is read without the source files present. Since validate by default now
    /// checks to make sure source files actually exist this will error out when verifying a dependency manifest. To get around this,
    /// the initial check of a dependency manifest that is done before cloning the dependency will ignore whether source files exist
    pub fn validate_ignore_sources(mut self) -> Result<Manifest> {
        self.sources = Some(SeqOrStruct::new(PartialSources::new_empty()));
        self.validate(&ValidationContext {
            pre_output: true,
            ..Default::default()
        })
    }
}

impl PrefixPaths for PartialManifest {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(PartialManifest {
            remotes: self.remotes,
            dependencies: self.dependencies.prefix_paths(prefix)?,
            sources: self.sources.prefix_paths(prefix)?,
            export_include_dirs: match self.export_include_dirs {
                Some(vec_inc) => Some(
                    vec_inc
                        .into_iter()
                        .map(|src| src.prefix_paths(prefix))
                        .collect::<Result<_>>()?,
                ),
                None => None,
            },
            plugins: self.plugins.prefix_paths(prefix)?,
            workspace: self.workspace.prefix_paths(prefix)?,
            vendor_package: self.vendor_package.prefix_paths(prefix)?,
            ..self
        })
    }
}

impl Validate for PartialManifest {
    type Output = Manifest;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<Manifest> {
        let pkg = match self.package {
            Some(mut p) => {
                p.name = p.name.to_lowercase();
                if !vctx.pre_output {
                    p.extra.iter().for_each(|(k, _)| {
                        Warnings::IgnoreUnknownField {
                            field: k.clone(),
                            pkg: p.name.clone(),
                        }
                        .emit();
                    });
                }
                p
            }
            None => bail!("Missing package information."),
        };

        let remotes: Option<IndexMap<String, RemoteConfig>> = self
            .remotes
            .map(|r| {
                r.validate(vctx).map_err(|(key, cause)| {
                    cause.wrap_err(format!("In remote `{key}` of package `{}`.", pkg.name))
                })
            })
            .transpose()?;

        let default_remote = if let Some(r_map) = &remotes {
            // If there is only one remote, it becomes the default.
            if r_map.len() == 1 {
                Some(r_map.values().next().unwrap())
            } else {
                // Otherwise, we validate that there is at most one default remote.
                let defaults: Vec<_> = r_map.values().filter(|r| r.default).collect();
                // Check how many default remotes there are.
                match defaults.as_slice() {
                    // No default remote. Allowed, but only allows explicit remote selection.
                    [] => None,
                    // Exactly one default remote.
                    [remote] => Some(*remote),
                    // Multiple default remotes. Not allowed.
                    _ => {
                        bail!("Multiple remotes marked as default. Only one allowed.",);
                    }
                }
            }
        } else {
            None
        };

        let deps = match self.dependencies {
            Some(d) => d
                .into_iter()
                .map(|(k, v)| {
                    let dep_name = k.to_lowercase();

                    // We need to construct a new context for the dependency validation
                    // since we need the dependency name and the remote definitions
                    // to validate a dependency.
                    let dep_vctx = ValidationContext {
                        package_name: &dep_name,
                        pre_output: vctx.pre_output,
                        remotes: remotes.as_ref(),
                        default_remote,
                    };

                    let validated = v.validate(&dep_vctx).wrap_err_with(|| {
                        format!("In dependency `{dep_name}` of package `{}`.", pkg.name)
                    })?;

                    Ok((dep_name, validated))
                })
                .collect::<Result<IndexMap<_, _>>>()?,
            None => IndexMap::new(),
        };
        let srcs = match self.sources {
            Some(s) => Some(
                s.validate(vctx)
                    .wrap_err_with(|| format!("In source list of package `{}`.", pkg.name))?,
            ),
            None => None,
        };
        let exp_inc_dirs = self
            .export_include_dirs
            .unwrap_or_default()
            .validate(vctx)
            .wrap_err(format!("In export_include_dirs of package `{}`:", pkg.name))?;
        let plugins = match self.plugins {
            Some(s) => s
                .iter()
                .map(|(k, v)| Ok((k.clone(), env_path_from_string(v)?)))
                .collect::<Result<IndexMap<_, _>>>()?,
            None => IndexMap::new(),
        };
        let frozen = self.frozen.unwrap_or(false);
        let workspace = match self.workspace {
            Some(w) => w.validate(vctx).wrap_err("In workspace configuration.")?,
            None => Workspace::default(),
        };
        let vendor_package = match self.vendor_package {
            Some(vend) => vend
                .validate(vctx)
                .wrap_err("Unable to parse vendor_package.")?,
            None => Vec::new(),
        };
        if !vctx.pre_output {
            self.extra.iter().for_each(|(k, _)| {
                Warnings::IgnoreUnknownField {
                    field: k.clone(),
                    pkg: pkg.name.clone(),
                }
                .emit();
            });
        }
        Ok(Manifest {
            package: pkg,
            dependencies: deps,
            sources: match srcs {
                Some(SourceFile::Group(srcs)) => Some(*srcs),
                Some(SourceFile::File(_))
                | Some(SourceFile::SvFile(_))
                | Some(SourceFile::VerilogFile(_))
                | Some(SourceFile::VhdlFile(_)) => Some(Sources {
                    target: TargetSpec::Wildcard,
                    include_dirs: Vec::new(),
                    defines: IndexMap::new(),
                    files: vec![srcs.unwrap()],
                    override_files: false,
                }),
                None => None,
            },
            export_include_dirs: exp_inc_dirs
                .into_iter()
                .map(|(trgt, path)| {
                    if !(vctx.pre_output || path.exists() && path.is_dir()) {
                        Errors::IncludeDirMissing(path.clone()).downgrade_if_suppressed()?;
                    }
                    Ok((trgt, path))
                })
                .collect::<Result<Vec<_>>>()?,
            plugins,
            frozen,
            workspace,
            vendor_package,
        })
    }
}

/// A partial dependency.
///
/// Contains all the necessary information to resolve and find a dependency.
/// The following combinations of fields are valid:
///
/// - `version`
/// - `path`
/// - `git,rev`
/// - `git,version`
///
/// Can be validated into a `Dependency`.
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct PartialDependency {
    /// The targets for which the dependency should be considered.
    target: Option<TargetSpec>,
    /// The path to the package.
    path: Option<String>,
    /// The git URL to the package.
    git: Option<String>,
    /// The git revision of the package to use. Can be a commit hash, branch,
    /// tag, or similar.
    rev: Option<String>,
    /// The version requirement of the package. This will be parsed into a
    /// semantic versioning requirement.
    version: Option<String>,
    /// The remote to use for this dependency
    remote: Option<String>,
    /// The upstream name of the remote to use for this dependency
    upstream_name: Option<String>,
    /// Targets to pass to the dependency
    pass_targets: Option<Vec<StringOrStruct<PartialPassedTarget>>>,
    /// Unknown extra fields
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

/// The `FromStr` implementation allows specifying a dependency as a simple
/// version string. For example:
/// ```yaml
/// my_dep: "1.2.3"
/// ```
/// will be interpreted as:
/// ```yaml
/// my_dep: { version: "1.2.3" }
/// ```
impl FromStr for PartialDependency {
    type Err = Void;
    fn from_str(s: &str) -> std::result::Result<Self, Void> {
        Ok(PartialDependency {
            version: Some(s.into()),
            ..Default::default()
        })
    }
}

impl PrefixPaths for PartialDependency {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(PartialDependency {
            path: self.path.prefix_paths(prefix)?,
            ..self
        })
    }
}

impl Validate for PartialDependency {
    type Output = Dependency;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<Dependency> {
        let target = self.target.unwrap_or(TargetSpec::Wildcard);
        let pass_targets = self
            .pass_targets
            .unwrap_or_default()
            .into_iter()
            .map(|s| s.validate(vctx))
            .collect::<Result<Vec<_>>>()?;
        let version = self
            .version
            .map(|v| {
                semver::VersionReq::parse(&v)
                    .into_diagnostic()
                    .wrap_err_with(|| {
                        format!("\"{}\" is not a valid semantic version requirement.", v)
                    })
            })
            .transpose()?;
        if !vctx.pre_output {
            self.extra.iter().for_each(|(k, _)| {
                Warnings::IgnoreUnknownField {
                    field: k.clone(),
                    pkg: vctx.package_name.to_string(),
                }
                .emit();
            });
        }

        match (self.git, self.path, self.rev, version, self.remote) {
            // Git dependencies with default remote, e.g.:
            // ```yaml
            // my_dep: "1.2.3"
            // my_dep: {version: "1.2.3"}
            // ```
            (None, None, None, Some(version), None) => {
                if let Some(default_remote) = vctx.default_remote {
                    let git_name = self.upstream_name.as_deref().unwrap_or(vctx.package_name);
                    Ok(Dependency::GitVersion {
                        target,
                        url: default_remote.url.replace("{}", git_name),
                        version,
                        pass_targets,
                    })
                } else {
                    Err(err!(
                        "A dependency with only a `version` field requires a default remote to be set.",
                    ))
                }
            }
            // Git dependencies with specified remote, e.g.:
            // ```yaml
            // my_dep: {version: "1.2.3", remote: "my_remote"}
            // ```
            (None, None, None, Some(version), Some(remote_name)) => {
                if let Some(remote) = vctx.remotes.and_then(|r| r.get(&remote_name)) {
                    let git_name = self.upstream_name.as_deref().unwrap_or(vctx.package_name);
                    Ok(Dependency::GitVersion {
                        target,
                        url: remote.url.replace("{}", git_name),
                        version,
                        pass_targets,
                    })
                } else {
                    Err(err!(
                        "Remote `{remote_name}` not found for dependency `{}`.",
                        vctx.package_name
                    ))
                }
            }
            // Git dependencies with explicit git and version, e.g.:
            // ```yaml
            // my_dep: {git: "http://example.com/repo.git", version: "1.2.3"}
            // ```
            (Some(git), None, None, Some(version), None) => Ok(Dependency::GitVersion {
                target,
                url: git,
                version,
                pass_targets,
            }),
            // Git dependencies with revisions, e.g.:
            // ```yaml
            // my_dep: {git: "http://example.com/repo.git", rev: "my_branch"}
            // my_dep: {git: "http://example.com/repo.git", rev: "some_hash"}
            // ```
            (Some(git), None, Some(rev), None, None) => Ok(Dependency::GitRevision {
                target,
                url: git,
                rev,
                pass_targets,
            }),
            // Path dependencies, e.g.:
            // ```yaml
            // my_dep: {path: "../my_dep"}
            // ```
            (None, Some(path), None, None, _) => Ok(Dependency::Path {
                target,
                path: env_path_from_string(&path)?,
                pass_targets,
            }),
            (_, _, Some(_), Some(_), _) => Err(err!(
                "Dependency `{}` cannot specify both `version` and `rev` fields.",
                vctx.package_name
            )),
            (git, Some(_), rev, version, _)
                if git.is_some() || rev.is_some() || version.is_some() =>
            {
                Err(err!(
                    "Dependency `{}` cannot simultaneously specify `path` with `git`, `rev`, or `version` fields.",
                    vctx.package_name
                ))
            }
            (Some(_), _, None, None, _) => Err(err!(
                "Dependency `{}` with `git` must also specify `rev` or `version`.",
                vctx.package_name
            )),
            (None, None, None, None, _) => Err(err!(
                "Dependency `{}` must specify at least one of `path`, `git`, `rev`, or `version`.",
                vctx.package_name
            )),
            (Some(_), _, _, _, Some(_)) => Err(err!(
                "Dependency `{}` cannot simultaneously specify `git` and `remote`",
                vctx.package_name
            )),
            cfg => Err(err!(
                "Invalid configuration for dependency `{}`: {cfg:?}",
                vctx.package_name
            )),
        }
    }
}

/// A partial filtered include directory (used for both source group and export include dirs)
#[derive(Serialize, Deserialize, Debug)]
pub struct PartialIncludeDir {
    /// The target spec for which the include dir applies
    pub target: Option<TargetSpec>,
    /// The include directory path
    pub dir: String,
    /// Unknown extra fields
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

impl FromStr for PartialIncludeDir {
    type Err = Void;
    fn from_str(s: &str) -> std::result::Result<Self, Void> {
        Ok(PartialIncludeDir {
            target: None,
            dir: s.into(),
            extra: HashMap::new(),
        })
    }
}

impl PrefixPaths for PartialIncludeDir {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(PartialIncludeDir {
            dir: self.dir.prefix_paths(prefix)?,
            ..self
        })
    }
}

impl Validate for PartialIncludeDir {
    type Output = (TargetSpec, PathBuf);
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<(TargetSpec, PathBuf)> {
        let dir = env_path_from_string(&self.dir)?;
        if !vctx.pre_output {
            self.extra.iter().for_each(|(k, _)| {
                Warnings::IgnoreUnknownField {
                    field: k.clone(),
                    pkg: vctx.package_name.to_string(),
                }
                .emit();
            });
        }
        Ok((self.target.unwrap_or(TargetSpec::Wildcard), dir))
    }
}

/// A partial filtered preprocessor define
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct PartialDefine {
    /// The target spec for which the define applies
    pub target: Option<TargetSpec>,
    /// The define value (None for valueless defines)
    pub value: Option<String>,
    /// Unknown extra fields
    #[serde(flatten)]
    pub extra: HashMap<String, Value>,
}

impl FromStr for PartialDefine {
    type Err = Void;
    fn from_str(s: &str) -> std::result::Result<Self, Void> {
        Ok(PartialDefine {
            target: None,
            value: Some(s.into()),
            extra: HashMap::new(),
        })
    }
}

impl Validate for PartialDefine {
    type Output = (TargetSpec, Option<String>);
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<(TargetSpec, Option<String>)> {
        if !vctx.pre_output {
            self.extra.iter().for_each(|(k, _)| {
                Warnings::IgnoreUnknownField {
                    field: k.clone(),
                    pkg: vctx.package_name.to_string(),
                }
                .emit();
            });
        }
        Ok((self.target.unwrap_or(TargetSpec::Wildcard), self.value))
    }
}

/// A partial group of source files.
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct PartialSources {
    /// The targets for which the sources should be considered.
    pub target: Option<TargetSpec>,
    /// The directories to search for include files.
    pub include_dirs: Option<Vec<StringOrStruct<PartialIncludeDir>>>,
    /// The preprocessor definitions.
    pub defines: Option<IndexMap<String, Option<StringOrStruct<PartialDefine>>>>,
    /// The source file paths.
    pub files: Option<Vec<PartialSourceFile>>,
    /// A sv file.
    pub sv: Option<String>,
    /// A verilog file.
    pub v: Option<String>,
    /// A vhdl file.
    pub vhd: Option<String>,
    /// The list of external flists to include.
    pub external_flists: Option<Vec<String>>,
    /// The files in this source will override other files.
    pub override_files: Option<bool>,
    /// Unknown extra fields
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

impl PartialSources {
    /// Create new empty PartialSources struct so partial sources can be emptied out
    /// when calling validate on db of git repo since source files won't actually be present
    pub fn new_empty() -> Self {
        PartialSources {
            files: Some(Vec::new()),
            ..Default::default()
        }
    }
}

impl PrefixPaths for PartialSources {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(PartialSources {
            include_dirs: self.include_dirs.prefix_paths(prefix)?,
            files: self.files.prefix_paths(prefix)?,
            sv: self.sv.prefix_paths(prefix)?,
            v: self.v.prefix_paths(prefix)?,
            vhd: self.vhd.prefix_paths(prefix)?,
            external_flists: self.external_flists.prefix_paths(prefix)?,
            ..self
        })
    }
}

impl From<Vec<PartialSourceFile>> for PartialSources {
    fn from(v: Vec<PartialSourceFile>) -> Self {
        PartialSources {
            files: Some(v),
            ..Default::default()
        }
    }
}

impl Validate for PartialSources {
    type Output = SourceFile;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<SourceFile> {
        match self {
            PartialSources {
                target: None,
                include_dirs: None,
                defines: None,
                files: None,
                sv: Some(sv),
                v: None,
                vhd: None,
                external_flists: None,
                override_files: None,
                extra: _,
            } => PartialSourceFile::SvFile(sv).validate(vctx),
            PartialSources {
                target: None,
                include_dirs: None,
                defines: None,
                files: None,
                sv: None,
                v: Some(v),
                vhd: None,
                external_flists: None,
                override_files: None,
                extra: _,
            } => PartialSourceFile::VerilogFile(v).validate(vctx),
            PartialSources {
                target: None,
                include_dirs: None,
                defines: None,
                files: None,
                sv: None,
                v: None,
                vhd: Some(vhd),
                external_flists: None,
                override_files: None,
                extra: _,
            } => PartialSourceFile::VhdlFile(vhd).validate(vctx),
            PartialSources {
                target,
                include_dirs,
                defines,
                files,
                sv: None,
                v: None,
                vhd: None,
                external_flists,
                override_files,
                extra,
            } => {
                let external_flists: Result<Vec<_>> = external_flists
                    .clone()
                    .unwrap_or_default()
                    .iter()
                    .filter_map(|path| match env_path_from_string(path) {
                        Ok(p) => Some(Ok(p)),
                        Err(cause) => {
                            let err = Errors::IgnoredPath {
                                cause: cause.to_string(),
                            };
                            err.downgrade_if_suppressed().err().map(Err)
                        }
                    })
                    .collect();

                let external_flist_list: Result<Vec<(PathBuf, Vec<String>)>> = external_flists?
                    .into_iter()
                    .map(|filename| {
                        let file = File::open(&filename).into_diagnostic().wrap_err_with(|| {
                            format!("Unable to open external flist file {:?}", filename)
                        })?;
                        let reader = BufReader::new(file);
                        let lines: Vec<String> = reader
                            .lines()
                            .map(|line| {
                                line.into_diagnostic().wrap_err_with(|| {
                                    format!("Error reading external flist file {:?}", filename)
                                })
                            })
                            .collect::<Result<Vec<String>>>()?;
                        let lines = lines
                            .iter()
                            .filter_map(|line| {
                                let line = line.trim();
                                if line.is_empty()
                                    || line.starts_with('#')
                                    || line.starts_with("//")
                                {
                                    None
                                } else {
                                    Some(
                                        line.split_whitespace()
                                            .map(|s| s.to_string())
                                            .collect::<Vec<_>>(),
                                    )
                                }
                            })
                            .flatten()
                            .collect();
                        Ok((filename.parent().unwrap().to_path_buf(), lines))
                    })
                    .collect();

                let external_flist_groups: Result<Vec<PartialSourceFile>> = external_flist_list?
                    .into_iter()
                    .map(|(flist_dir, flist)| {
                        Ok(PartialSourceFile::Group(Box::new(PartialSources {
                            include_dirs: Some(
                                flist
                                    .clone()
                                    .into_iter()
                                    .filter_map(|file| {
                                        if file.starts_with("+incdir+") {
                                            Some(file.trim_start_matches("+incdir+").to_string())
                                        } else {
                                            None
                                        }
                                    })
                                    .flat_map(|s| {
                                        s.split('+').map(|s| s.to_string()).collect::<Vec<_>>()
                                    })
                                    .map(|dir| {
                                        let prefixed = dir.prefix_paths(&flist_dir)?;
                                        Ok(StringOrStruct(
                                            PartialIncludeDir::from_str(&prefixed).unwrap(),
                                        ))
                                    })
                                    .collect::<Result<_>>()?,
                            ),
                            defines: Some(
                                flist
                                    .clone()
                                    .into_iter()
                                    .filter_map(|file| {
                                        if file.starts_with("+define+") {
                                            Some(file.trim_start_matches("+define+").to_string())
                                        } else {
                                            None
                                        }
                                    })
                                    .flat_map(|s| {
                                        s.split('+').map(|s| s.to_string()).collect::<Vec<_>>()
                                    })
                                    .map(|file| {
                                        if let Some(eq_idx) = file.find("=") {
                                            (
                                                file[..eq_idx].to_string(),
                                                Some(StringOrStruct(PartialDefine {
                                                    value: Some(file[eq_idx + 1..].to_string()),
                                                    ..Default::default()
                                                })),
                                            )
                                        } else {
                                            (file.to_string(), None)
                                        }
                                    })
                                    .collect(),
                            ),
                            files: Some(
                                flist
                                    .into_iter()
                                    .filter_map(|file| {
                                        if file.starts_with("+") {
                                            None
                                        } else {
                                            // prefix path
                                            Some(PartialSourceFile::File(file))
                                        }
                                    })
                                    .map(|file| file.prefix_paths(&flist_dir))
                                    .collect::<Result<Vec<_>>>()?,
                            ),
                            ..Default::default()
                        })))
                    })
                    .collect();

                let post_env_files: Vec<PartialSourceFile> = if let Some(fls) = files {
                    fls.into_iter()
                        .chain(external_flist_groups?)
                        .filter_map(|file| match file {
                            PartialSourceFile::File(ref filename)
                            | PartialSourceFile::SvFile(ref filename)
                            | PartialSourceFile::VerilogFile(ref filename)
                            | PartialSourceFile::VhdlFile(ref filename) => {
                                match env_string_from_string(filename) {
                                    Ok(p) => match file {
                                        PartialSourceFile::File(_) => {
                                            Some(Ok(PartialSourceFile::File(p)))
                                        }
                                        PartialSourceFile::SvFile(_) => {
                                            Some(Ok(PartialSourceFile::SvFile(p)))
                                        }
                                        PartialSourceFile::VerilogFile(_) => {
                                            Some(Ok(PartialSourceFile::VerilogFile(p)))
                                        }
                                        PartialSourceFile::VhdlFile(_) => {
                                            Some(Ok(PartialSourceFile::VhdlFile(p)))
                                        }
                                        _ => unreachable!(),
                                    },
                                    Err(cause) => {
                                        let err = Errors::IgnoredPath {
                                            cause: cause.to_string(),
                                        };
                                        err.downgrade_if_suppressed().err().map(Err)
                                    }
                                }
                            }
                            other => Some(Ok(other)),
                        })
                        .collect::<Result<Vec<_>>>()?
                } else {
                    Vec::new()
                };
                let post_glob_files: Vec<PartialSourceFile> = post_env_files
                    .into_iter()
                    .map(|pre_glob_file| {
                        match pre_glob_file {
                            PartialSourceFile::File(_)
                            | PartialSourceFile::SvFile(_)
                            | PartialSourceFile::VerilogFile(_)
                            | PartialSourceFile::VhdlFile(_) => {
                                // PartialSources .files item is pointing to PartialSourceFiles::file so do glob extension
                                pre_glob_file.glob_file()
                            }
                            _ => {
                                // PartialSources .files item is pointing to PartialSourceFiles::group so pass on for recursion
                                // to do glob extension in the groups.sources.files list of PartialSourceFiles::file
                                Ok(vec![pre_glob_file])
                            }
                        }
                    })
                    .collect::<Result<Vec<Vec<PartialSourceFile>>>>()?
                    .into_iter()
                    .flatten()
                    .collect::<Vec<_>>();

                let include_dirs = include_dirs
                    .unwrap_or_default()
                    .into_iter()
                    .filter_map(|entry| match entry.0.validate(vctx) {
                        Ok(validated) => Some(Ok(validated)),
                        Err(cause) => {
                            let err = Errors::IgnoredPath {
                                cause: cause.to_string(),
                            };
                            err.downgrade_if_suppressed().err().map(Err)
                        }
                    })
                    .collect::<Result<Vec<_>>>()?;

                let defines: IndexMap<String, (TargetSpec, Option<String>)> = defines
                    .unwrap_or_default()
                    .into_iter()
                    .map(|(name, partial)| {
                        let (trgt, value) = if let Some(partial) = partial {
                            partial.validate(vctx)?
                        } else {
                            (TargetSpec::Wildcard, None)
                        };
                        Ok((name, (trgt, value)))
                    })
                    .collect::<Result<_>>()?;
                let files: Result<Vec<_>> = post_glob_files
                    .into_iter()
                    .map(|f| f.validate(vctx))
                    .collect();
                let files: Vec<SourceFile> = files?;
                let files: Vec<SourceFile> = files.into_iter().collect();
                if files.is_empty() && !vctx.pre_output {
                    Warnings::NoFilesInSourceGroup(vctx.package_name.to_string()).emit();
                }
                if !vctx.pre_output {
                    extra.iter().for_each(|(k, _)| {
                        Warnings::IgnoreUnknownField {
                            field: k.clone(),
                            pkg: vctx.package_name.to_string(),
                        }
                        .emit();
                    });
                }
                if override_files == Some(true) && (!include_dirs.is_empty() || !defines.is_empty())
                {
                    Warnings::OverrideFilesWithExtras(vctx.package_name.to_string()).emit();
                }
                Ok(SourceFile::Group(Box::new(Sources {
                    target: target.unwrap_or_default(),
                    include_dirs,
                    defines,
                    files,
                    override_files: override_files.is_some_and(|x| x),
                })))
            }
            PartialSources {
                target: None,
                include_dirs: None,
                defines: None,
                files: None,
                sv: _sv,
                v: _v,
                vhd: _vhd,
                external_flists: None,
                override_files: None,
                extra: _,
            } => Err(err!(
                "Only a single source with a single type is supported.",
            )),
            _ => Err(err!(
                "Do not mix `sv`, `v`, or `vhd` with `files`, `target`, `include_dirs`, and `defines`.",
            )),
        }
    }
}

/// A partial source file.
#[derive(Debug)]
pub enum PartialSourceFile {
    /// A single file.
    File(String),
    /// A subgroup of sources.
    Group(Box<PartialSources>),
    /// A systemverilog source.
    SvFile(String),
    /// A verilog source.
    VerilogFile(String),
    /// A vhdl source.
    VhdlFile(String),
}

impl PrefixPaths for PartialSourceFile {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(match self {
            PartialSourceFile::File(path) => PartialSourceFile::File(path.prefix_paths(prefix)?),
            PartialSourceFile::Group(group) => {
                PartialSourceFile::Group(Box::new(group.prefix_paths(prefix)?))
            }
            PartialSourceFile::SvFile(path) => {
                PartialSourceFile::SvFile(path.prefix_paths(prefix)?)
            }
            PartialSourceFile::VerilogFile(path) => {
                PartialSourceFile::VerilogFile(path.prefix_paths(prefix)?)
            }
            PartialSourceFile::VhdlFile(path) => {
                PartialSourceFile::VhdlFile(path.prefix_paths(prefix)?)
            }
        })
    }
}

// Custom serialization for partial source files.
impl Serialize for PartialSourceFile {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match *self {
            PartialSourceFile::File(ref path) => path.serialize(serializer),
            PartialSourceFile::Group(ref srcs) => srcs.serialize(serializer),
            PartialSourceFile::SvFile(ref path) => path.serialize(serializer),
            PartialSourceFile::VerilogFile(ref path) => path.serialize(serializer),
            PartialSourceFile::VhdlFile(ref path) => path.serialize(serializer),
        }
    }
}

// Custom deserialization for partial source files.
impl<'de> Deserialize<'de> for PartialSourceFile {
    fn deserialize<D>(deserializer: D) -> std::result::Result<PartialSourceFile, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de;
        use std::result::Result;
        struct Visitor;

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

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("string or map")
            }

            // Parse a single source file.
            fn visit_str<E>(self, value: &str) -> Result<PartialSourceFile, E>
            where
                E: de::Error,
            {
                Ok(PartialSourceFile::File(value.into()))
            }

            // Parse an entire source file group.
            fn visit_map<M>(self, visitor: M) -> Result<PartialSourceFile, M::Error>
            where
                M: de::MapAccess<'de>,
            {
                let srcs =
                    PartialSources::deserialize(de::value::MapAccessDeserializer::new(visitor))?;
                Ok(PartialSourceFile::Group(Box::new(srcs)))
            }
        }

        deserializer.deserialize_any(Visitor)
    }
}

impl Validate for PartialSourceFile {
    type Output = SourceFile;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<SourceFile> {
        match self {
            PartialSourceFile::File(path) => Ok(SourceFile::File(PathBuf::from(path))),
            // PartialSourceFile::Group(srcs) => Ok(Some(SourceFile::Group(Box::new(
            //     srcs.validate(package_name, pre_output, suppress_warnings)?,
            // )))),
            PartialSourceFile::Group(srcs) => Ok(srcs.validate(vctx)?),
            PartialSourceFile::SvFile(path) => Ok(SourceFile::SvFile(env_path_from_string(&path)?)),
            PartialSourceFile::VerilogFile(path) => {
                Ok(SourceFile::VerilogFile(env_path_from_string(&path)?))
            }
            PartialSourceFile::VhdlFile(path) => {
                Ok(SourceFile::VhdlFile(env_path_from_string(&path)?))
            }
        }
    }
}

/// Converts a file with glob in name to a list of matching files
pub trait GlobFile {
    /// The output type produced by validation.
    type Output;
    /// The error type produced by validation.
    type Error;
    /// Validate self and convert to a full list of paths that exist
    fn glob_file(self) -> Result<Self::Output>;
}

impl GlobFile for PartialSourceFile {
    type Output = Vec<PartialSourceFile>;
    type Error = Error;

    fn glob_file(self) -> Result<Vec<PartialSourceFile>> {
        // let mut partial_source_files_vec: Vec<PartialSourceFile> = Vec::new();

        // Only operate on files, not groups
        match self {
            PartialSourceFile::File(ref path)
            | PartialSourceFile::SvFile(ref path)
            | PartialSourceFile::VerilogFile(ref path)
            | PartialSourceFile::VhdlFile(ref path)
                if path.contains("*") || path.contains("?") =>
            {
                let glob_matches = glob(path)
                    .into_diagnostic()
                    .wrap_err_with(|| format!("Invalid glob pattern for {:?}", path))?;
                let out = glob_matches
                    .map(|glob_match| {
                        let file_str = glob_match
                            .into_diagnostic()
                            .wrap_err_with(|| format!("Glob match failed for {:?}", path))?
                            .to_str()
                            .unwrap()
                            .to_string();
                        Ok(match self {
                            PartialSourceFile::File(_) => PartialSourceFile::File(file_str),
                            PartialSourceFile::SvFile(_) => PartialSourceFile::SvFile(file_str),
                            PartialSourceFile::VerilogFile(_) => {
                                PartialSourceFile::VerilogFile(file_str)
                            }
                            PartialSourceFile::VhdlFile(_) => PartialSourceFile::VhdlFile(file_str),
                            _ => unreachable!(),
                        })
                    })
                    .collect::<Result<Vec<PartialSourceFile>>>()?;
                if out.is_empty() {
                    Warnings::NoFilesForGlobPattern { path: path.clone() }.emit();
                }
                Ok(out)
            }
            // Return self if not a glob pattern (also matches non-file variants).
            _ => Ok(vec![self]),
        }
    }
}

/// A partial workspace configuration.
#[derive(Serialize, Deserialize, Debug)]
pub struct PartialWorkspace {
    /// The directory which will contain working copies of the dependencies.
    pub checkout_dir: Option<String>,
    /// The locally linked packages.
    pub package_links: Option<IndexMap<String, String>>,
    /// Unknown extra fields
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

impl PrefixPaths for PartialWorkspace {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(PartialWorkspace {
            checkout_dir: self.checkout_dir.prefix_paths(prefix)?,
            package_links: match self.package_links {
                Some(idx_map) => Some(
                    idx_map
                        .into_iter()
                        .map(|(k, v)| Ok((k.prefix_paths(prefix)?, v)))
                        .collect::<Result<_>>()?,
                ),
                None => None,
            },
            ..self
        })
    }
}

impl Validate for PartialWorkspace {
    type Output = Workspace;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<Workspace> {
        let package_links: Result<IndexMap<_, _>> = self
            .package_links
            .unwrap_or_default()
            .iter()
            .map(|(k, v)| Ok((env_path_from_string(k)?, v.clone())))
            .collect();
        if !vctx.pre_output {
            self.extra.iter().for_each(|(k, _)| {
                Warnings::IgnoreUnknownField {
                    field: k.clone(),
                    pkg: vctx.package_name.to_string(),
                }
                .emit();
            });
        }
        Ok(Workspace {
            checkout_dir: match self.checkout_dir {
                Some(dir) => Some(env_path_from_string(&dir)?),
                None => None,
            },
            package_links: package_links?,
        })
    }
}

/// Merges missing information from another struct.
pub trait Merge {
    /// Populate missing fields from `other`.
    fn merge(self, other: Self) -> Self;
}

/// Prefixes relative paths.
pub trait PrefixPaths {
    /// Prefixes all paths with `prefix`. Does not touch absolute paths.
    fn prefix_paths(self, prefix: &Path) -> Result<Self>
    where
        Self: std::marker::Sized;
}

impl PrefixPaths for PathBuf {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(prefix.join(self))
    }
}

impl PrefixPaths for String {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        // Resolve env vars first so absolute results bypass the prefix.
        let substituted = env_string_from_string(&self)?;
        let path = PathBuf::from(&substituted);
        if path.is_absolute() {
            Ok(substituted)
        } else {
            Ok(prefix.join(&path).display().to_string())
        }
    }
}

impl<T> PrefixPaths for Option<T>
where
    T: PrefixPaths,
{
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        self.map_or(Ok(None), |inner| Ok(Some(inner.prefix_paths(prefix)?)))
    }
}

// Implement `PrefixPaths` for hash maps of prefixable values.
impl<K, V> PrefixPaths for IndexMap<K, V>
where
    K: Hash + Eq,
    V: PrefixPaths,
{
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        self.into_iter()
            .map(|(k, v)| Ok((k, v.prefix_paths(prefix)?)))
            .collect()
    }
}

// Implement `PrefixPaths` for vectors of prefixable values.
impl<V> PrefixPaths for Vec<V>
where
    V: PrefixPaths,
{
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        self.into_iter().map(|v| v.prefix_paths(prefix)).collect()
    }
}

/// A configuration.
///
/// This struct encapsulates every setting of the tool that can be changed by
/// the user by some means. It is constructed from a partial configuration.
#[derive(Serialize, Debug)]
pub struct Config {
    /// The path to the database directory.
    pub database: PathBuf,
    /// Optional override for the directory containing bare git repositories
    /// and their lock files. When set, takes precedence over `database` for
    /// these two specific paths; checkouts and everything else still derive
    /// from `database`.
    pub db_dir: Option<PathBuf>,
    /// The git command or path to the binary.
    pub git: String,
    /// The dependency overrides.
    pub overrides: IndexMap<String, Dependency>,
    /// The auxiliary plugin dependencies.
    pub plugins: IndexMap<String, Dependency>,
    /// The git throttle value to use unless overridden by the user.
    pub git_throttle: Option<usize>,
    /// Enable git LFS support, requires git-lfs (default: true)
    pub git_lfs: bool,
}

/// A partial configuration.
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct PartialConfig {
    /// The path to the database directory.
    pub database: Option<String>,
    /// Optional override for the bare-repo and lock directory.
    pub db_dir: Option<String>,
    /// The git command or path to the binary.
    pub git: Option<String>,
    /// The dependency overrides.
    pub overrides: Option<IndexMap<String, PartialDependency>>,
    /// The auxiliary plugin dependencies.
    pub plugins: Option<IndexMap<String, PartialDependency>>,
    /// The git throttle value to use unless overridden by the user.
    pub git_throttle: Option<usize>,
    /// Enable git LFS support, requires git-lfs (default: true)
    pub git_lfs: Option<bool>,
}

impl PartialConfig {
    /// Create a new empty configuration.
    pub fn new() -> PartialConfig {
        Default::default()
    }
}

impl PrefixPaths for PartialConfig {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        Ok(PartialConfig {
            database: self.database.prefix_paths(prefix)?,
            db_dir: self.db_dir.prefix_paths(prefix)?,
            overrides: self.overrides.prefix_paths(prefix)?,
            plugins: self.plugins.prefix_paths(prefix)?,
            ..self
        })
    }
}

impl Merge for PartialConfig {
    fn merge(self, other: PartialConfig) -> PartialConfig {
        PartialConfig {
            database: self.database.or(other.database),
            db_dir: self.db_dir.or(other.db_dir),
            git: self.git.or(other.git),
            overrides: match (self.overrides, other.overrides) {
                (Some(o), None) | (None, Some(o)) => Some(o),
                (Some(mut o1), Some(o2)) => {
                    o1.extend(o2);
                    Some(o1)
                }
                (None, None) => None,
            },
            plugins: match (self.plugins, other.plugins) {
                (Some(o), None) | (None, Some(o)) => Some(o),
                (Some(mut o1), Some(o2)) => {
                    o1.extend(o2);
                    Some(o1)
                }
                (None, None) => None,
            },
            git_throttle: self.git_throttle.or(other.git_throttle),
            git_lfs: match (self.git_lfs, other.git_lfs) {
                (Some(v), None) | (None, Some(v)) => Some(v),
                (Some(v1), Some(v2)) => Some(v1 | v2),
                (None, None) => None,
            },
        }
    }
}

impl Validate for PartialConfig {
    type Output = Config;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<Config> {
        Ok(Config {
            database: match self.database {
                Some(db) => env_path_from_string(&db)?,
                None => bail!("Database directory not configured"),
            },
            db_dir: match self.db_dir {
                Some(d) => Some(env_path_from_string(&d)?),
                None => None,
            },
            git: match self.git {
                Some(git) => git,
                None => bail!("Git command or path to binary not configured"),
            },
            overrides: match self.overrides {
                Some(d) => d
                    .validate(vctx)
                    .map_err(|(key, cause)| cause.wrap_err(format!("In override `{key}`.")))?,
                None => IndexMap::new(),
            },
            plugins: match self.plugins {
                Some(d) => d
                    .validate(vctx)
                    .map_err(|(key, cause)| cause.wrap_err(format!("In plugin `{key}`.")))?,
                None => IndexMap::new(),
            },
            git_throttle: self.git_throttle,
            git_lfs: self.git_lfs.unwrap_or(true),
        })
    }
}

/// An external import dependency
#[derive(Serialize, Debug)]
pub struct VendorPackage {
    /// External dependency name
    pub name: String,
    /// Target folder for imported dependency
    pub target_dir: PathBuf,
    /// Upstream dependency reference
    pub upstream: Dependency,
    /// Import mapping
    pub mapping: Vec<FromToLink>,
    /// Folder containing patch files
    pub patch_dir: Option<PathBuf>,
    /// include from upstream
    pub include_from_upstream: Vec<String>,
    /// exclude from upstream
    pub exclude_from_upstream: Vec<String>,
}

impl PrefixPaths for VendorPackage {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        let patch_root = self.patch_dir.prefix_paths(prefix)?;
        Ok(VendorPackage {
            target_dir: self.target_dir.prefix_paths(prefix)?,
            mapping: self
                .mapping
                .into_iter()
                .map(|ftl| {
                    Ok(FromToLink {
                        patch_dir: ftl.patch_dir.map_or(
                            Ok::<Option<PathBuf>, Error>(None),
                            |dir| {
                                Ok(Some({
                                    dir.prefix_paths(&patch_root.clone().expect(
                            "A mapping has a local patch_dir, but no global patch_dir is defined.",
                        ))?
                                }))
                            },
                        )?,
                        ..ftl
                    })
                })
                .collect::<Result<_>>()?,
            patch_dir: patch_root,
            ..self
        })
    }
}

/// A partial external import dependency
#[derive(Serialize, Deserialize, Debug)]
pub struct PartialVendorPackage {
    /// External dependency name
    pub name: Option<String>,
    /// Target folder for imported dependency
    pub target_dir: Option<String>,
    /// Upstream dependency reference
    pub upstream: Option<PartialDependency>,
    /// Import mapping
    pub mapping: Option<Vec<FromToLink>>,
    /// Folder containing patch files
    pub patch_dir: Option<String>,
    // /// Dependency containing patches
    // pub patch_repo: Option<PartialDependency>,
    /// include from upstream
    pub include_from_upstream: Option<Vec<String>>,
    /// exclude from upstream
    pub exclude_from_upstream: Option<Vec<String>>,
}

impl PrefixPaths for PartialVendorPackage {
    fn prefix_paths(self, prefix: &Path) -> Result<Self> {
        let patch_root = self.patch_dir.prefix_paths(prefix)?;
        Ok(PartialVendorPackage {
            target_dir: self.target_dir.prefix_paths(prefix)?,
            mapping: match self.mapping {
                Some(mapping_vec) => Some(mapping_vec
                    .into_iter()
                    .map(|ftl| {
                        Ok(FromToLink {
                            patch_dir: ftl.patch_dir.map_or(
                                Ok::<Option<PathBuf>, Error>(None),
                                |dir| {
                                    Ok(Some({
                                        dir.prefix_paths(Path::new(&patch_root.clone().expect(
                                "A mapping has a local patch_dir, but no global patch_dir is defined.",
                            )))?
                                    }))
                                },
                            )?,
                            ..ftl
                        })
                    })
                    .collect::<Result<_>>()?),
                None => None,
            },
            patch_dir: patch_root,
            ..self
        })
    }
}

impl Validate for PartialVendorPackage {
    type Output = VendorPackage;
    type Error = Error;
    fn validate(self, vctx: &ValidationContext) -> Result<VendorPackage> {
        Ok(VendorPackage {
            name: match self.name {
                Some(name) => name,
                None => bail!("external import name missing"),
            },
            target_dir: match self.target_dir {
                Some(target_dir) => env_path_from_string(&target_dir)?,
                None => bail!("external import target dir missing"),
            },
            upstream: match self.upstream {
                Some(upstream) => upstream
                    .validate(vctx)
                    .wrap_err("Unable to parse external import upstream.")?,
                None => bail!("external import upstream missing"),
            },
            mapping: self.mapping.unwrap_or_default(),
            patch_dir: match self.patch_dir {
                Some(patch_dir) => Some(env_path_from_string(&patch_dir)?),
                None => None,
            },
            include_from_upstream: match self.include_from_upstream {
                Some(include_from_upstream) => include_from_upstream,
                None => vec![String::from("")],
            },
            exclude_from_upstream: {
                let mut excl = self.exclude_from_upstream.unwrap_or_default();
                excl.push(String::from(".git"));
                excl
            },
        })
    }
}

/// An external import linkage
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct FromToLink {
    /// from string
    pub from: PathBuf,
    /// to string
    pub to: PathBuf,
    /// directory
    pub patch_dir: Option<PathBuf>,
}

/// A passed target
#[derive(Clone, Default, Serialize, Debug, Hash, PartialEq, Eq)]
pub struct PassedTarget {
    /// Target name
    pub target: TargetSpec,
    /// Target value
    pub pass: String,
}

/// A partial passed target
#[derive(Serialize, Deserialize, Debug)]
pub struct PartialPassedTarget {
    /// Filtering target specification
    pub target: Option<TargetSpec>,
    /// Target to pass
    pub pass: Option<String>,
}

impl Validate for PartialPassedTarget {
    type Output = PassedTarget;
    type Error = Error;
    fn validate(self, _vctx: &ValidationContext) -> Result<PassedTarget> {
        Ok(PassedTarget {
            target: self.target.unwrap_or_default(),
            pass: match self.pass {
                Some(p) => p.to_lowercase(),
                None => bail!("passed target missing pass value"),
            },
        })
    }
}

impl FromStr for PartialPassedTarget {
    type Err = Void;
    fn from_str(s: &str) -> std::result::Result<Self, Void> {
        Ok(PartialPassedTarget {
            target: None,
            pass: Some(s.to_string()),
        })
    }
}

impl fmt::Display for PassedTarget {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "target: `{}`: `{}`", self.target, self.pass)
    }
}

/// A remote index configuration.
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct RemoteConfig {
    /// The URL prefix of the remote index.
    pub url: String,
    /// Whether this remote is the default.
    #[serde(default)]
    pub default: bool,
}

impl FromStr for RemoteConfig {
    type Err = Void;
    fn from_str(s: &str) -> std::result::Result<Self, Void> {
        Ok(RemoteConfig {
            url: s.into(),
            default: false,
        })
    }
}

impl Validate for RemoteConfig {
    type Output = RemoteConfig;
    type Error = Error;
    fn validate(self, _vctx: &ValidationContext) -> Result<RemoteConfig> {
        let raw_url = self.url.trim();

        let template_url = match raw_url.matches("{}").count() {
            0 => {
                let base_url = raw_url.trim_end_matches('/');
                format!("{}/{}.git", base_url, "{}")
            }
            1 => raw_url.to_string(),
            _ => {
                bail!(
                    "Remote URL '{}' contains multiple '{{}}' placeholders. Only one is allowed.",
                    raw_url
                );
            }
        };

        Ok(RemoteConfig {
            url: template_url,
            default: self.default,
        })
    }
}

/// A lock file.
///
/// This struct encapsulates the result of dependency resolution. For every
/// dependency in the package it lists the exact source and version.
#[derive(Serialize, Deserialize, Debug)]
pub struct Locked {
    /// The locked package versions.
    pub packages: BTreeMap<String, LockedPackage>,
}

/// A locked dependency.
///
/// Encapsualtes the exact source and version of a dependency.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct LockedPackage {
    /// The revision hash of the dependency.
    pub revision: Option<String>,
    /// The version of the dependency.
    pub version: Option<String>,
    /// The source of the dependency.
    #[serde(with = "serde_yaml_ng::with::singleton_map")]
    pub source: LockedSource,
    /// Other packages this package depends on.
    pub dependencies: BTreeSet<String>,
}

/// A source description for a locked dependency.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum LockedSource {
    /// A path on the system.
    Path(PathBuf),
    /// A git URL.
    Git(String),
    /// A registry.
    Registry(String),
}

#[cfg(unix)]
fn env_string_from_string(path_str: &str) -> Result<String> {
    subst::substitute(path_str, &subst::Env)
        .into_diagnostic()
        .wrap_err_with(|| format!("Unable to substitute with env: {}", path_str))
}

#[cfg(windows)]
fn env_string_from_string(path_str: &str) -> Result<String> {
    Ok(path_str.to_string())
}

pub(crate) fn env_path_from_string(path_str: &str) -> Result<PathBuf> {
    Ok(PathBuf::from(env_string_from_string(path_str)?))
}
