use crate::core::{Dependency, Package, PackageId, SourceId};
use crate::sources::IndexSummary;
use crate::sources::source::MaybePackage;
use crate::sources::source::QueryKind;
use crate::sources::source::Source;
use crate::util::errors::CargoResult;

/// A source that replaces one source with the other. This manages the [source
/// replacement] feature.
///
/// The implementation is merely redirecting from the original to the replacement.
///
/// [source replacement]: https://doc.rust-lang.org/nightly/cargo/reference/source-replacement.html
pub struct ReplacedSource<'gctx> {
    /// The identifier of the original source.
    to_replace: SourceId,
    /// The identifier of the new replacement source.
    replace_with: SourceId,
    inner: Box<dyn Source + 'gctx>,
}

impl<'gctx> ReplacedSource<'gctx> {
    /// Creates a replaced source.
    ///
    /// The `src` argument is the new replacement source.
    pub fn new(
        to_replace: SourceId,
        replace_with: SourceId,
        src: Box<dyn Source + 'gctx>,
    ) -> ReplacedSource<'gctx> {
        ReplacedSource {
            to_replace,
            replace_with,
            inner: src,
        }
    }

    /// Is this source a built-in replacement of crates.io?
    ///
    /// Built-in source replacement of crates.io for sparse registry or tests
    /// should not show messages indicating source replacement.
    fn is_builtin_replacement(&self) -> bool {
        self.replace_with.is_crates_io() && self.to_replace.is_crates_io()
    }
}

#[async_trait::async_trait(?Send)]
impl<'gctx> Source for ReplacedSource<'gctx> {
    fn source_id(&self) -> SourceId {
        self.to_replace
    }

    fn replaced_source_id(&self) -> SourceId {
        self.replace_with
    }

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

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

    async fn query(
        &self,
        dep: &Dependency,
        kind: QueryKind,
        f: &mut dyn FnMut(IndexSummary),
    ) -> CargoResult<()> {
        let (replace_with, to_replace) = (self.replace_with, self.to_replace);
        let dep = dep.clone().map_source(to_replace, replace_with);

        self.inner
            .query(&dep, kind, &mut |summary| {
                f(summary.map_summary(|s| s.map_source(replace_with, to_replace)))
            })
            .await
            .map_err(|e| {
                if self.is_builtin_replacement() {
                    e
                } else {
                    e.context(format!(
                        "failed to query replaced source {}",
                        self.to_replace
                    ))
                }
            })
    }

    fn invalidate_cache(&self) {
        self.inner.invalidate_cache()
    }

    fn set_quiet(&mut self, quiet: bool) {
        self.inner.set_quiet(quiet);
    }

    async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
        let id = id.with_source_id(self.replace_with);
        let pkg = self.inner.download(id).await.map_err(|e| {
            if self.is_builtin_replacement() {
                e
            } else {
                e.context(format!(
                    "failed to download replaced source {}",
                    self.to_replace
                ))
            }
        })?;
        Ok(match pkg {
            MaybePackage::Ready(pkg) => {
                MaybePackage::Ready(pkg.map_source(self.replace_with, self.to_replace))
            }
            other @ MaybePackage::Download { .. } => other,
        })
    }

    async fn finish_download(&self, id: PackageId, data: Vec<u8>) -> CargoResult<Package> {
        let id = id.with_source_id(self.replace_with);
        let pkg = self.inner.finish_download(id, data).await.map_err(|e| {
            if self.is_builtin_replacement() {
                e
            } else {
                e.context(format!(
                    "failed to download replaced source {}",
                    self.to_replace
                ))
            }
        })?;
        Ok(pkg.map_source(self.replace_with, self.to_replace))
    }

    fn fingerprint(&self, id: &Package) -> CargoResult<String> {
        self.inner.fingerprint(id)
    }

    fn verify(&self, id: PackageId) -> CargoResult<()> {
        let id = id.with_source_id(self.replace_with);
        self.inner.verify(id)
    }

    fn describe(&self) -> String {
        if self.is_builtin_replacement() {
            self.inner.describe()
        } else {
            format!(
                "{} (which is replacing {})",
                self.inner.describe(),
                self.to_replace
            )
        }
    }

    fn is_replaced(&self) -> bool {
        !self.is_builtin_replacement()
    }
}
