use crate::Db;
use crate::Definition;
use crate::Name;
use crate::NamespaceVisibility;
use crate::Package;

#[salsa::tracked]
impl<'db> Package {
    /// Resolve `name` against this package's top-level bindings.
    ///
    /// - `Exported` (R's `pkg::name`) gates on the package's NAMESPACE `export()`
    ///   directives: an internal binding is invisible even if it exists.
    /// - `Internal` (R's `pkg:::name`) returns any top-level binding regardless
    ///   of NAMESPACE.
    ///
    /// Iterates [`Package::files`] and aggregates each file's
    /// [`File::resolve_export`] for `name`.
    ///
    /// Returns a `Vec` because a package's namespace can carry more than one
    /// binding per name. A common pattern is a top-level stub overridden from
    /// an `.onLoad` hook via `<<-`:
    ///
    /// ```r
    /// # R/foo.R
    /// foo <- function() stop("not loaded yet")
    ///
    /// # R/zzz.R
    /// .onLoad <- function(libname, pkgname) {
    ///   foo <<- function() "real implementation"
    /// }
    /// ```
    ///
    /// Both bindings live in the package namespace; both come back as
    /// candidates. Conditional fan-out within a single file (e.g.
    /// `if cond x <- 1 else x <- 2`) surfaces here the same way.
    ///
    /// Returns an empty Vec when the name isn't bound anywhere in the package,
    /// or when `Exported` is requested for a name absent from
    /// `namespace.exports`.
    #[salsa::tracked(returns(clone), cycle_result = resolve_cycle_result)]
    pub fn resolve(
        self,
        db: &'db dyn Db,
        name: Name<'db>,
        visibility: NamespaceVisibility,
    ) -> Vec<Definition<'db>> {
        // When resolving an export, the `name` must be present in the `NAMESPACE`
        // exports. If the package is `base`, there is no `NAMESPACE`, but all top-level
        // bindings in `base` are visible by construction, so we skip this check entirely.
        if visibility == NamespaceVisibility::Exported &&
            self.name(db) != "base" &&
            !self
                .namespace(db)
                .exports
                .contains_str(name.text(db).as_str())
        {
            return Vec::new();
        }

        let mut results = Vec::new();
        for &file in self.files(db) {
            results.extend(file.resolve_export(db, name));
        }

        // Re-exports. A name brought in by `importFrom()` and surfaced again by
        // `export()` has no binding in this package's own files, so the loop
        // above returns empty. Follow the import to the source package and
        // resolve the name among its exports instead.
        if results.is_empty() {
            let name_str = name.text(db).as_str();
            if let Some(source_name) = self.imported_from(db).get(name_str) {
                if let Some(source) = db.package_by_name(source_name) {
                    results = source.resolve(db, name, NamespaceVisibility::Exported);
                }
            }
        }

        results
    }
}

/// Return no candidates when re-export resolution revisits the same query key.
/// This matches the result for a re-export whose source package is not loaded.
fn resolve_cycle_result<'db>(
    db: &'db dyn Db,
    _id: salsa::Id,
    package: Package,
    name: Name<'db>,
    _visibility: NamespaceVisibility,
) -> Vec<Definition<'db>> {
    log::warn!(
        "Cyclic NAMESPACE re-export of `{}` detected at `{}`. Resolving to no candidates.",
        name.text(db).as_str(),
        package.name(db),
    );
    Vec::new()
}
