//! Attributes & documentation for hir types.

use cfg::CfgExpr;
use either::Either;
use hir_def::{
    AssocItemId, AttrDefId, FieldId, GenericDefId, ItemContainerId, LifetimeParamId, ModuleDefId,
    TraitId, TypeOrConstParamId,
    attrs::{AttrFlags, Docs, IsInnerDoc},
    expr_store::path::Path,
    hir::generics::GenericParams,
    item_scope::ItemInNs,
    per_ns::Namespace,
    resolver::{HasResolver, Resolver, TypeNs},
};
use hir_expand::{
    mod_path::{ModPath, PathKind},
    name::Name,
};
use hir_ty::{
    db::HirDatabase,
    method_resolution::{self, CandidateId, MethodError, MethodResolutionContext},
    next_solver::{DbInterner, TypingMode, infer::DbInternerInferExt},
};
use intern::Symbol;
use stdx::never;

use crate::{
    Adt, AsAssocItem, AssocItem, BuiltinType, Const, ConstParam, DocLinkDef, Enum, EnumVariant,
    ExternCrateDecl, Field, Function, GenericParam, HasCrate, Impl, LangItem, LifetimeParam, Macro,
    Module, ModuleDef, Static, Struct, Trait, Type, TypeAlias, TypeParam, Union, Variant,
};

#[derive(Debug, Clone, Copy)]
pub enum AttrsOwner {
    AttrDef(AttrDefId),
    Field(FieldId),
    LifetimeParam(LifetimeParamId),
    TypeOrConstParam(TypeOrConstParamId),
    /// Things that do not have attributes.
    ///
    /// Used for:
    /// - builtin derives
    /// - builtin types (as those do not have attributes)
    Dummy,
}

impl AttrsOwner {
    #[inline]
    fn attr_def(&self) -> Option<AttrDefId> {
        match self {
            AttrsOwner::AttrDef(it) => Some(*it),
            _ => None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct AttrsWithOwner {
    pub(crate) attrs: AttrFlags,
    owner: AttrsOwner,
}

impl AttrsWithOwner {
    fn new(db: &dyn HirDatabase, owner: AttrDefId) -> Self {
        Self { attrs: AttrFlags::query(db, owner), owner: AttrsOwner::AttrDef(owner) }
    }

    fn new_field(db: &dyn HirDatabase, owner: FieldId) -> Self {
        Self { attrs: AttrFlags::query_field(db, owner), owner: AttrsOwner::Field(owner) }
    }

    fn new_lifetime_param(db: &dyn HirDatabase, owner: LifetimeParamId) -> Self {
        Self {
            attrs: AttrFlags::query_lifetime_param(db, owner),
            owner: AttrsOwner::LifetimeParam(owner),
        }
    }
    fn new_type_or_const_param(db: &dyn HirDatabase, owner: TypeOrConstParamId) -> Self {
        Self {
            attrs: AttrFlags::query_type_or_const_param(db, owner),
            owner: AttrsOwner::TypeOrConstParam(owner),
        }
    }

    #[inline]
    pub fn is_unstable(&self) -> bool {
        self.attrs.contains(AttrFlags::IS_UNSTABLE)
    }

    /// Currently, it could be that `is_unstable() == true` but `unstable_feature == None`
    /// (due to unstable features not being retrieved for fields etc.).
    #[inline]
    pub fn unstable_feature(&self, db: &dyn HirDatabase) -> Option<Symbol> {
        match self.owner {
            AttrsOwner::AttrDef(owner) => self.attrs.unstable_feature(db, owner),
            AttrsOwner::Field(_)
            | AttrsOwner::LifetimeParam(_)
            | AttrsOwner::TypeOrConstParam(_)
            | AttrsOwner::Dummy => None,
        }
    }

    #[inline]
    pub fn is_macro_export(&self) -> bool {
        self.attrs.contains(AttrFlags::IS_MACRO_EXPORT)
    }

    #[inline]
    pub fn is_doc_notable_trait(&self) -> bool {
        self.attrs.contains(AttrFlags::IS_DOC_NOTABLE_TRAIT)
    }

    #[inline]
    pub fn is_doc_hidden(&self) -> bool {
        self.attrs.contains(AttrFlags::IS_DOC_HIDDEN)
    }

    #[inline]
    pub fn is_deprecated(&self) -> bool {
        self.attrs.contains(AttrFlags::IS_DEPRECATED)
    }

    #[inline]
    pub fn is_non_exhaustive(&self) -> bool {
        self.attrs.contains(AttrFlags::NON_EXHAUSTIVE)
    }

    #[inline]
    pub fn is_test(&self) -> bool {
        self.attrs.contains(AttrFlags::IS_TEST)
    }

    #[inline]
    pub fn lang(&self, db: &dyn HirDatabase) -> Option<LangItem> {
        self.owner
            .attr_def()
            .and_then(|owner| self.attrs.lang_item_with_attrs(db, owner))
            .and_then(|lang| LangItem::from_symbol(&lang))
    }

    #[inline]
    pub fn doc_aliases<'db>(&self, db: &'db dyn HirDatabase) -> &'db [Symbol] {
        let owner = match self.owner {
            AttrsOwner::AttrDef(it) => Either::Left(it),
            AttrsOwner::Field(it) => Either::Right(it),
            AttrsOwner::LifetimeParam(_) | AttrsOwner::TypeOrConstParam(_) | AttrsOwner::Dummy => {
                return &[];
            }
        };
        self.attrs.doc_aliases(db, owner)
    }

    #[inline]
    pub fn cfgs<'db>(&self, db: &'db dyn HirDatabase) -> Option<&'db CfgExpr> {
        let owner = match self.owner {
            AttrsOwner::AttrDef(it) => Either::Left(it),
            AttrsOwner::Field(it) => Either::Right(it),
            AttrsOwner::LifetimeParam(_) | AttrsOwner::TypeOrConstParam(_) | AttrsOwner::Dummy => {
                return None;
            }
        };
        self.attrs.cfgs(db, owner)
    }

    #[inline]
    pub fn hir_docs<'db>(&self, db: &'db dyn HirDatabase) -> Option<&'db Docs> {
        match self.owner {
            AttrsOwner::AttrDef(it) => AttrFlags::docs(db, it).as_deref(),
            AttrsOwner::Field(it) => AttrFlags::field_docs(db, it),
            AttrsOwner::LifetimeParam(_) | AttrsOwner::TypeOrConstParam(_) | AttrsOwner::Dummy => {
                None
            }
        }
    }
}

pub trait HasAttrs: Sized {
    #[inline]
    fn attrs(self, db: &dyn HirDatabase) -> AttrsWithOwner {
        match self.attr_id(db) {
            AttrsOwner::AttrDef(it) => AttrsWithOwner::new(db, it),
            AttrsOwner::Field(it) => AttrsWithOwner::new_field(db, it),
            AttrsOwner::LifetimeParam(it) => AttrsWithOwner::new_lifetime_param(db, it),
            AttrsOwner::TypeOrConstParam(it) => AttrsWithOwner::new_type_or_const_param(db, it),
            AttrsOwner::Dummy => {
                AttrsWithOwner { attrs: AttrFlags::empty(), owner: AttrsOwner::Dummy }
            }
        }
    }

    #[doc(hidden)]
    fn attr_id(self, db: &dyn HirDatabase) -> AttrsOwner;

    #[inline]
    fn hir_docs(self, db: &dyn HirDatabase) -> Option<&Docs> {
        match self.attr_id(db) {
            AttrsOwner::AttrDef(it) => AttrFlags::docs(db, it).as_deref(),
            AttrsOwner::Field(it) => AttrFlags::field_docs(db, it),
            AttrsOwner::LifetimeParam(_) | AttrsOwner::TypeOrConstParam(_) | AttrsOwner::Dummy => {
                None
            }
        }
    }
}

macro_rules! impl_has_attrs {
    ($(($def:ident, $def_id:ident),)*) => {$(
        impl HasAttrs for $def {
            #[inline]
            fn attr_id(self, _db: &dyn HirDatabase) -> AttrsOwner {
                AttrsOwner::AttrDef(AttrDefId::$def_id(self.into()))
            }
        }
    )*};
}

impl_has_attrs![
    (EnumVariant, EnumVariantId),
    (Static, StaticId),
    (Const, ConstId),
    (Trait, TraitId),
    (TypeAlias, TypeAliasId),
    (Macro, MacroId),
    (Adt, AdtId),
    (ExternCrateDecl, ExternCrateId),
];

impl HasAttrs for Function {
    fn attr_id(self, _db: &dyn HirDatabase) -> AttrsOwner {
        match self.id {
            crate::AnyFunctionId::FunctionId(id) => AttrsOwner::AttrDef(id.into()),
            crate::AnyFunctionId::BuiltinDeriveImplMethod { .. } => AttrsOwner::Dummy,
        }
    }
}

impl HasAttrs for Impl {
    fn attr_id(self, _db: &dyn HirDatabase) -> AttrsOwner {
        match self.id {
            hir_ty::next_solver::AnyImplId::ImplId(id) => AttrsOwner::AttrDef(id.into()),
            hir_ty::next_solver::AnyImplId::BuiltinDeriveImplId(..) => AttrsOwner::Dummy,
        }
    }
}

macro_rules! impl_has_attrs_enum {
    ($($variant:ident),* for $enum:ident) => {$(
        impl HasAttrs for $variant {
            #[inline]
            fn attr_id(self, db: &dyn HirDatabase) -> AttrsOwner {
                $enum::$variant(self).attr_id(db)
            }
        }
    )*};
}

impl_has_attrs_enum![Struct, Union, Enum for Adt];
impl_has_attrs_enum![TypeParam, ConstParam, LifetimeParam for GenericParam];

impl HasAttrs for Module {
    #[inline]
    fn attr_id(self, _: &dyn HirDatabase) -> AttrsOwner {
        AttrsOwner::AttrDef(AttrDefId::ModuleId(self.id))
    }
}

impl HasAttrs for GenericParam {
    #[inline]
    fn attr_id(self, _db: &dyn HirDatabase) -> AttrsOwner {
        match self {
            GenericParam::TypeParam(it) => AttrsOwner::TypeOrConstParam(it.merge().into()),
            GenericParam::ConstParam(it) => AttrsOwner::TypeOrConstParam(it.merge().into()),
            GenericParam::LifetimeParam(it) => AttrsOwner::LifetimeParam(it.into()),
        }
    }
}

impl HasAttrs for AssocItem {
    #[inline]
    fn attr_id(self, db: &dyn HirDatabase) -> AttrsOwner {
        match self {
            AssocItem::Function(it) => it.attr_id(db),
            AssocItem::Const(it) => it.attr_id(db),
            AssocItem::TypeAlias(it) => it.attr_id(db),
        }
    }
}

impl HasAttrs for crate::Crate {
    #[inline]
    fn attr_id(self, db: &dyn HirDatabase) -> AttrsOwner {
        self.root_module(db).attr_id(db)
    }
}

impl HasAttrs for Field {
    #[inline]
    fn attr_id(self, _db: &dyn HirDatabase) -> AttrsOwner {
        AttrsOwner::Field(self.into())
    }
}

/// Resolves the item `link` points to in the scope of `def`.
pub fn resolve_doc_path_on(
    db: &dyn HirDatabase,
    def: impl HasAttrs + Copy,
    link: &str,
    ns: Option<Namespace>,
    is_inner_doc: IsInnerDoc,
) -> Option<DocLinkDef> {
    resolve_doc_path_on_(db, link, def.attr_id(db), ns, is_inner_doc)
}

fn resolve_doc_path_on_(
    db: &dyn HirDatabase,
    link: &str,
    attr_id: AttrsOwner,
    ns: Option<Namespace>,
    is_inner_doc: IsInnerDoc,
) -> Option<DocLinkDef> {
    let resolver = match attr_id {
        AttrsOwner::AttrDef(AttrDefId::ModuleId(it)) => {
            if is_inner_doc.yes() {
                it.resolver(db)
            } else if let Some(parent) = Module::from(it).parent(db) {
                parent.id.resolver(db)
            } else {
                it.resolver(db)
            }
        }
        AttrsOwner::AttrDef(AttrDefId::AdtId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::FunctionId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::EnumVariantId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::StaticId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::ConstId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::TraitId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::TypeAliasId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::ImplId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::ExternBlockId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::UseId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::MacroId(it)) => it.resolver(db),
        AttrsOwner::AttrDef(AttrDefId::ExternCrateId(it)) => it.resolver(db),
        AttrsOwner::Field(it) => it.parent.resolver(db),
        AttrsOwner::LifetimeParam(_) | AttrsOwner::TypeOrConstParam(_) | AttrsOwner::Dummy => {
            return None;
        }
    };

    let mut modpath = doc_modpath_from_str(link)?;

    let resolved = resolver.resolve_module_path_in_items(db, &modpath);
    if resolved.is_none() {
        let last_name = modpath.pop_segment()?;
        resolve_assoc_or_field(db, resolver, modpath, last_name, ns)
    } else {
        let def = match ns {
            Some(Namespace::Types) => resolved.take_types(),
            Some(Namespace::Values) => resolved.take_values(),
            Some(Namespace::Macros) => resolved.take_macros().map(ModuleDefId::MacroId),
            None => resolved.iter_items().next().map(|(it, _)| match it {
                ItemInNs::Types(it) => it,
                ItemInNs::Values(it) => it,
                ItemInNs::Macros(it) => ModuleDefId::MacroId(it),
            }),
        };
        Some(DocLinkDef::ModuleDef(def?.into()))
    }
}

fn resolve_assoc_or_field(
    db: &dyn HirDatabase,
    resolver: Resolver<'_>,
    path: ModPath,
    name: Name,
    ns: Option<Namespace>,
) -> Option<DocLinkDef> {
    let path = Path::from_known_path_with_no_generic(path);
    let base_def = resolver.resolve_path_in_type_ns_fully(db, &path)?;

    let handle_trait = |id: TraitId| {
        // Doc paths in this context may only resolve to an item of this trait
        // (i.e. no items of its supertraits), so we need to handle them here
        // independently of others.
        id.trait_items(db).items.iter().find(|it| it.0 == name).map(|(_, assoc_id)| {
            let def = match *assoc_id {
                AssocItemId::FunctionId(it) => ModuleDef::Function(it.into()),
                AssocItemId::ConstId(it) => ModuleDef::Const(it.into()),
                AssocItemId::TypeAliasId(it) => ModuleDef::TypeAlias(it.into()),
            };
            DocLinkDef::ModuleDef(def)
        })
    };
    let ty = match base_def {
        TypeNs::SelfType(id) => Impl::from(id).self_ty(db),
        TypeNs::GenericParam(param) => {
            let generic_params = GenericParams::of(db, param.parent());
            if generic_params[param.local_id()].is_trait_self() {
                // `Self::assoc` in traits should refer to the trait itself.
                let parent_trait = |container| match container {
                    ItemContainerId::TraitId(trait_) => handle_trait(trait_),
                    _ => {
                        never!("container {container:?} should be a trait");
                        None
                    }
                };
                return match param.parent() {
                    GenericDefId::TraitId(trait_) => handle_trait(trait_),
                    GenericDefId::ConstId(it) => parent_trait(it.loc(db).container),
                    GenericDefId::FunctionId(it) => parent_trait(it.loc(db).container),
                    GenericDefId::TypeAliasId(it) => parent_trait(it.loc(db).container),
                    _ => {
                        never!("type param {param:?} should belong to a trait");
                        None
                    }
                };
            }

            // Even if this generic parameter has some trait bounds, rustdoc doesn't
            // resolve `name` to trait items.
            return None;
        }
        TypeNs::AdtId(id) | TypeNs::AdtSelfType(id) => Adt::from(id).ty(db),
        TypeNs::EnumVariantId(id) => {
            // Enum variants don't have path candidates.
            let variant = EnumVariant::from(id);
            return resolve_field(db, variant.into(), name, ns);
        }
        TypeNs::TypeAliasId(id) => {
            let alias = TypeAlias::from(id);
            if alias.as_assoc_item(db).is_some() {
                // We don't normalize associated type aliases, so we have nothing to
                // resolve `name` to.
                return None;
            }
            alias.ty(db)
        }
        TypeNs::BuiltinType(id) => BuiltinType::from(id).ty(db),
        TypeNs::TraitId(id) => return handle_trait(id),
        TypeNs::ModuleId(_) => {
            return None;
        }
    };

    // Resolve inherent items first, then trait items, then fields.
    if let Some(assoc_item_def) = resolve_assoc_item(db, &ty, &name, ns) {
        return Some(assoc_item_def);
    }

    if let Some(impl_trait_item_def) = resolve_impl_trait_item(db, resolver, &ty, &name, ns) {
        return Some(impl_trait_item_def);
    }

    let variant_def = match ty.as_adt()? {
        Adt::Struct(it) => it.into(),
        Adt::Union(it) => it.into(),
        Adt::Enum(enum_) => {
            // Can happen on `Self::Variant` (otherwise would be fully resolved by the resolver).
            return enum_
                .id
                .enum_variants(db)
                .variant(&name)
                .map(|variant| DocLinkDef::ModuleDef(ModuleDef::EnumVariant(variant.into())));
        }
    };
    resolve_field(db, variant_def, name, ns)
}

fn resolve_assoc_item<'db>(
    db: &'db dyn HirDatabase,
    ty: &Type<'db>,
    name: &Name,
    ns: Option<Namespace>,
) -> Option<DocLinkDef> {
    ty.iterate_assoc_items(db, move |assoc_item| {
        if assoc_item.name(db)? != *name {
            return None;
        }
        as_module_def_if_namespace_matches(assoc_item, ns)
    })
}

fn resolve_impl_trait_item<'db>(
    db: &'db dyn HirDatabase,
    resolver: Resolver<'_>,
    ty: &Type<'db>,
    name: &Name,
    ns: Option<Namespace>,
) -> Option<DocLinkDef> {
    let krate = ty.krate(db);
    let environment = crate::param_env_from_resolver(db, &resolver);
    let traits_in_scope = resolver.traits_in_scope(db);

    // `ty.iterate_path_candidates()` require a scope, which is not available when resolving
    // attributes here. Use path resolution directly instead.
    //
    // FIXME: resolve type aliases (which are not yielded by iterate_path_candidates)
    let interner = DbInterner::new_with(db, environment.krate);
    let infcx = interner.infer_ctxt().build(TypingMode::PostAnalysis);
    let features = resolver.top_level_def_map().features();
    let ctx = MethodResolutionContext {
        infcx: &infcx,
        resolver: &resolver,
        param_env: environment.param_env,
        traits_in_scope: &traits_in_scope,
        edition: krate.edition(db),
        features,
        call_span: hir_ty::Span::Dummy,
        receiver_span: hir_ty::Span::Dummy,
    };
    let resolution = ctx.probe_for_name(method_resolution::Mode::Path, name.clone(), ty.ty);
    let resolution = match resolution {
        Ok(resolution) => resolution.item,
        Err(MethodError::PrivateMatch(resolution)) => resolution.item,
        _ => return None,
    };
    let resolution = match resolution {
        CandidateId::FunctionId(id) => AssocItem::Function(id.into()),
        CandidateId::ConstId(id) => AssocItem::Const(id.into()),
    };
    as_module_def_if_namespace_matches(resolution, ns)
}

fn resolve_field(
    db: &dyn HirDatabase,
    def: Variant,
    name: Name,
    ns: Option<Namespace>,
) -> Option<DocLinkDef> {
    if let Some(Namespace::Types | Namespace::Macros) = ns {
        return None;
    }
    def.fields(db).into_iter().find(|f| f.name(db) == name).map(DocLinkDef::Field)
}

fn as_module_def_if_namespace_matches(
    assoc_item: AssocItem,
    ns: Option<Namespace>,
) -> Option<DocLinkDef> {
    let (def, expected_ns) = match assoc_item {
        AssocItem::Function(it) => (ModuleDef::Function(it), Namespace::Values),
        AssocItem::Const(it) => (ModuleDef::Const(it), Namespace::Values),
        AssocItem::TypeAlias(it) => (ModuleDef::TypeAlias(it), Namespace::Types),
    };

    (ns.unwrap_or(expected_ns) == expected_ns).then_some(DocLinkDef::ModuleDef(def))
}

fn doc_modpath_from_str(link: &str) -> Option<ModPath> {
    // FIXME: this is not how we should get a mod path here.
    let try_get_modpath = |link: &str| {
        let mut parts = link.split("::");
        let mut first_segment = None;
        let kind = match parts.next()? {
            "" => PathKind::Abs,
            "crate" => PathKind::Crate,
            "self" => PathKind::SELF,
            "super" => {
                let mut deg = 1;
                for segment in parts.by_ref() {
                    if segment == "super" {
                        deg += 1;
                    } else {
                        first_segment = Some(segment);
                        break;
                    }
                }
                PathKind::Super(deg)
            }
            segment => {
                first_segment = Some(segment);
                PathKind::Plain
            }
        };
        let parts = first_segment.into_iter().chain(parts).map(|segment| match segment.parse() {
            Ok(idx) => Name::new_tuple_field(idx),
            Err(_) => Name::new_root(segment.split_once('<').map_or(segment, |it| it.0)),
        });
        Some(ModPath::from_segments(kind, parts))
    };
    try_get_modpath(link)
}
