//! Access to a registry on the local filesystem. See [`LocalRegistry`] for more.

use crate::core::PackageId;
use crate::sources::registry::{LoadResponse, MaybeLock, RegistryConfig, RegistryData};
use crate::util::errors::CargoResult;
use crate::util::{Filesystem, GlobalContext};
use cargo_util::{Sha256, paths};
use std::cell::Cell;
use std::fs::File;
use std::io::SeekFrom;
use std::io::{self, prelude::*};
use std::path::Path;

/// A local registry is a registry that lives on the filesystem as a set of
/// `.crate` files with an `index` directory in the [same format] as a remote
/// registry.
///
/// This type is primarily accessed through the [`RegistryData`] trait.
///
/// When a local registry is requested for a package, it simply looks into what
/// its index has under the `index` directory. When [`LocalRegistry::download`]
/// is called, a local registry verifies the checksum of the requested `.crate`
/// tarball and then unpacks it to `$CARGO_HOME/.registry/src`.
///
/// > Note that there is a third-party subcommand [`cargo-local-registry`],
/// > which happened to be developed by a former Cargo team member when local
/// > registry was introduced. The tool is to ease the burden of maintaining
/// > local registries. However, in general the Cargo team avoids recommending
/// > any specific third-party crate. Just FYI.
///
/// [same format]: super#the-format-of-the-index
/// [`cargo-local-registry`]: https://crates.io/crates/cargo-local-registry
///
/// # Filesystem hierarchy
///
/// Here is an example layout of a local registry on a local filesystem:
///
/// ```text
/// [registry root]/
/// ├── index/                      # registry index
/// │  ├── an/
/// │  │  └── yh/
/// │  │     └── anyhow
/// │  ├── ru/
/// │  │  └── st/
/// │  │     ├── rustls
/// │  │     └── rustls-ffi
/// │  └── se/
/// │     └── mv/
/// │        └── semver
/// ├── anyhow-1.0.71.crate         # pre-downloaded crate tarballs
/// ├── rustls-0.20.8.crate
/// ├── rustls-ffi-0.8.2.crate
/// └── semver-1.0.17.crate
/// ```
///
/// For general concepts of registries, see the [module-level documentation](crate::sources::registry).
pub struct LocalRegistry<'gctx> {
    /// Path to the registry index.
    index_path: Filesystem,
    /// Root path of this local registry.
    root: Filesystem,
    /// Path where this local registry extract `.crate` tarballs to.
    src_path: Filesystem,
    gctx: &'gctx GlobalContext,
    /// Whether this source has updated all package information it may contain.
    updated: Cell<bool>,
    /// Disables status messages.
    quiet: bool,
}

impl<'gctx> LocalRegistry<'gctx> {
    /// Creates a local registry at `root`.
    ///
    /// * `name` --- Name of a path segment where `.crate` tarballs are stored.
    ///   Expect to be unique.
    pub fn new(root: &Path, gctx: &'gctx GlobalContext, name: &str) -> LocalRegistry<'gctx> {
        LocalRegistry {
            src_path: gctx.registry_source_path().join(name),
            index_path: Filesystem::new(root.join("index")),
            root: Filesystem::new(root.to_path_buf()),
            gctx,
            updated: Cell::new(false),
            quiet: false,
        }
    }

    fn update(&self) -> CargoResult<()> {
        if self.updated.get() {
            return Ok(());
        }
        // Nothing to update, we just use what's on disk. Verify it actually
        // exists though. We don't use any locks as we're just checking whether
        // these directories exist.
        let root = self.root.clone().into_path_unlocked();
        if !root.is_dir() {
            anyhow::bail!("local registry path is not a directory: {}", root.display());
        }
        let index_path = self.index_path.clone().into_path_unlocked();
        if !index_path.is_dir() {
            anyhow::bail!(
                "local registry index path is not a directory: {}",
                index_path.display()
            );
        }
        self.updated.set(true);
        Ok(())
    }
}

#[async_trait::async_trait(?Send)]
impl<'gctx> RegistryData for LocalRegistry<'gctx> {
    fn prepare(&self) -> CargoResult<()> {
        Ok(())
    }

    fn index_path(&self) -> &Filesystem {
        &self.index_path
    }

    fn cache_path(&self) -> &Filesystem {
        &self.root
    }

    fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
        // Note that the `*_unlocked` variant is used here since we're not
        // modifying the index and it's required to be externally synchronized.
        path.as_path_unlocked()
    }

    async fn load(
        &self,
        root: &Path,
        path: &Path,
        _index_version: Option<&str>,
    ) -> CargoResult<LoadResponse> {
        if !self.updated.get() {
            self.update()?;
        }
        let raw_data = match paths::read_bytes(&root.join(path)) {
            Err(e)
                if e.downcast_ref::<io::Error>()
                    .map_or(false, |ioe| ioe.kind() == io::ErrorKind::NotFound) =>
            {
                return Ok(LoadResponse::NotFound);
            }
            r => r,
        }?;
        Ok(LoadResponse::Data {
            raw_data,
            index_version: None,
        })
    }

    async fn config(&self) -> CargoResult<Option<RegistryConfig>> {
        // Local registries don't have configuration for remote APIs or anything
        // like that
        Ok(None)
    }

    fn invalidate_cache(&self) {
        // Local registry has no cache - just reads from disk.
    }

    fn set_quiet(&mut self, _quiet: bool) {
        self.quiet = true;
    }

    fn is_updated(&self) -> bool {
        // There is nothing to update.
        true
    }

    async fn download(&self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock> {
        // Note that the usage of `into_path_unlocked` here is because the local
        // crate files here never change in that we're not the one writing them,
        // so it's not our responsibility to synchronize access to them.
        let path = self.root.join(&pkg.tarball_name()).into_path_unlocked();
        let mut crate_file = paths::open(&path)?;

        // If we've already got an unpacked version of this crate, then skip the
        // checksum below as it is in theory already verified.
        let dst = path.file_stem().unwrap();
        if self.src_path.join(dst).into_path_unlocked().exists() {
            return Ok(MaybeLock::Ready(crate_file));
        }

        if !self.quiet {
            self.gctx.shell().status("Unpacking", pkg)?;
        }

        // We don't actually need to download anything per-se, we just need to
        // verify the checksum matches the .crate file itself.
        let actual = Sha256::new().update_file(&crate_file)?.finish_hex();
        if actual != checksum {
            anyhow::bail!("failed to verify the checksum of `{}`", pkg)
        }

        crate_file.seek(SeekFrom::Start(0))?;

        Ok(MaybeLock::Ready(crate_file))
    }

    async fn finish_download(
        &self,
        _pkg: PackageId,
        _checksum: &str,
        _data: &[u8],
    ) -> CargoResult<File> {
        panic!("this source doesn't download")
    }
}
