use rustc_data_structures::sorted_map::SortedIndexMultiMap;
use rustc_hir as hir;
use rustc_hir::def::{DefKind, Namespace};
use rustc_hir::def_id::DefId;
use rustc_macros::{Decodable, Encodable, StableHash};
use rustc_span::{ErrorGuaranteed, Ident, Symbol};

use super::{TyCtxt, Visibility};
use crate::ty;

#[derive(Clone, Copy, PartialEq, Eq, Debug, StableHash, Hash, Encodable, Decodable)]
pub enum AssocContainer {
    Trait,
    InherentImpl,
    /// The `DefId` points to the trait item being implemented.
    TraitImpl(Result<DefId, ErrorGuaranteed>),
}

/// Information about an associated item
#[derive(Copy, Clone, Debug, PartialEq, StableHash, Eq, Hash, Encodable, Decodable)]
pub struct AssocItem {
    pub def_id: DefId,
    pub kind: AssocKind,
    pub container: AssocContainer,
}

impl AssocItem {
    // Gets the identifier, if it has one.
    pub fn opt_name(&self) -> Option<Symbol> {
        match self.kind {
            ty::AssocKind::Type { data: AssocTypeData::Normal(name) } => Some(name),
            ty::AssocKind::Type { data: AssocTypeData::Rpitit(_) } => None,
            ty::AssocKind::Const { name, .. } => Some(name),
            ty::AssocKind::Fn { name, .. } => Some(name),
        }
    }

    // Gets the identifier name. Aborts if it lacks one, i.e. is an RPITIT
    // associated type.
    pub fn name(&self) -> Symbol {
        self.opt_name().expect("name of non-Rpitit assoc item")
    }

    pub fn ident(&self, tcx: TyCtxt<'_>) -> Ident {
        Ident::new(self.name(), tcx.def_ident_span(self.def_id).unwrap())
    }

    /// Gets the defaultness of the associated item.
    /// To get the default associated type, use the [`type_of`] query on the
    /// [`DefId`] of the type.
    ///
    /// [`type_of`]: crate::ty::TyCtxt::type_of
    pub fn defaultness(&self, tcx: TyCtxt<'_>) -> hir::Defaultness {
        match self.container {
            AssocContainer::InherentImpl => hir::Defaultness::Final,
            AssocContainer::Trait | AssocContainer::TraitImpl(_) => tcx.defaultness(self.def_id),
        }
    }

    pub fn expect_trait_impl(&self) -> Result<DefId, ErrorGuaranteed> {
        let AssocContainer::TraitImpl(trait_item_id) = self.container else {
            bug!("expected item to be in a trait impl: {:?}", self.def_id);
        };
        trait_item_id
    }

    /// If this is a trait impl item, returns the `DefId` of the trait item this implements.
    /// Otherwise, returns `DefId` for self. Returns an Err in case the trait item was not
    /// resolved successfully.
    pub fn trait_item_or_self(&self) -> Result<DefId, ErrorGuaranteed> {
        match self.container {
            AssocContainer::TraitImpl(id) => id,
            AssocContainer::Trait | AssocContainer::InherentImpl => Ok(self.def_id),
        }
    }

    pub fn trait_item_def_id(&self) -> Option<DefId> {
        match self.container {
            AssocContainer::TraitImpl(Ok(id)) => Some(id),
            _ => None,
        }
    }

    #[inline]
    pub fn visibility(&self, tcx: TyCtxt<'_>) -> Visibility<DefId> {
        tcx.visibility(self.def_id)
    }

    #[inline]
    pub fn container_id(&self, tcx: TyCtxt<'_>) -> DefId {
        tcx.parent(self.def_id)
    }

    #[inline]
    pub fn trait_container(&self, tcx: TyCtxt<'_>) -> Option<DefId> {
        match self.container {
            AssocContainer::InherentImpl | AssocContainer::TraitImpl(_) => None,
            AssocContainer::Trait => Some(tcx.parent(self.def_id)),
        }
    }

    #[inline]
    pub fn impl_container(&self, tcx: TyCtxt<'_>) -> Option<DefId> {
        match self.container {
            AssocContainer::InherentImpl | AssocContainer::TraitImpl(_) => {
                Some(tcx.parent(self.def_id))
            }
            AssocContainer::Trait => None,
        }
    }

    pub fn signature(&self, tcx: TyCtxt<'_>) -> String {
        match self.kind {
            ty::AssocKind::Fn { .. } => {
                // We skip the binder here because the binder would deanonymize all
                // late-bound regions, and we don't want method signatures to show up
                // `as for<'r> fn(&'r MyType)`. Pretty-printing handles late-bound
                // regions just fine, showing `fn(&MyType)`.
                tcx.fn_sig(self.def_id).instantiate_identity().skip_binder().to_string()
            }
            ty::AssocKind::Type { .. } => format!("type {};", self.name()),
            ty::AssocKind::Const { name, .. } => {
                format!("const {}: {:?};", name, tcx.type_of(self.def_id).instantiate_identity())
            }
        }
    }

    pub fn descr(&self) -> &'static str {
        self.kind.descr()
    }

    pub fn namespace(&self) -> Namespace {
        self.kind.namespace()
    }

    pub fn as_def_kind(&self) -> DefKind {
        self.kind.as_def_kind()
    }

    pub fn is_type_const(&self) -> bool {
        matches!(self.kind, ty::AssocKind::Const { is_type_const: true, .. })
    }

    pub fn is_fn(&self) -> bool {
        matches!(self.kind, ty::AssocKind::Fn { .. })
    }

    pub fn is_method(&self) -> bool {
        matches!(self.kind, ty::AssocKind::Fn { has_self: true, .. })
    }

    pub fn is_type(&self) -> bool {
        matches!(self.kind, ty::AssocKind::Type { .. })
    }

    pub fn tag(&self) -> AssocTag {
        self.kind.tag()
    }

    pub fn is_impl_trait_in_trait(&self) -> bool {
        matches!(self.kind, AssocKind::Type { data: AssocTypeData::Rpitit(_) })
    }
}

#[derive(Copy, Clone, PartialEq, Debug, StableHash, Eq, Hash, Encodable, Decodable)]
pub enum AssocTypeData {
    Normal(Symbol),
    /// The associated type comes from an RPITIT. It has no name, and the
    /// `ImplTraitInTraitData` provides additional information about its
    /// source.
    Rpitit(ty::ImplTraitInTraitData),
}

#[derive(Copy, Clone, PartialEq, Debug, StableHash, Eq, Hash, Encodable, Decodable)]
pub enum AssocKind {
    Const { name: Symbol, is_type_const: bool },
    Fn { name: Symbol, has_self: bool },
    Type { data: AssocTypeData },
}

impl AssocKind {
    pub fn namespace(&self) -> Namespace {
        match self {
            Self::Type { .. } => Namespace::TypeNS,
            Self::Const { .. } | Self::Fn { .. } => Namespace::ValueNS,
        }
    }

    pub fn tag(&self) -> AssocTag {
        match self {
            Self::Const { .. } => AssocTag::Const,
            Self::Fn { .. } => AssocTag::Fn,
            Self::Type { .. } => AssocTag::Type,
        }
    }

    pub fn as_def_kind(&self) -> DefKind {
        match self {
            Self::Const { is_type_const, .. } => {
                DefKind::AssocConst { is_type_const: *is_type_const }
            }
            Self::Fn { .. } => DefKind::AssocFn,
            Self::Type { .. } => DefKind::AssocTy,
        }
    }

    pub fn descr(&self) -> &'static str {
        match self {
            Self::Fn { has_self: true, .. } => "method",
            _ => self.tag().descr(),
        }
    }
}

impl std::fmt::Display for AssocKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.descr())
    }
}

/// Like [`AssocKind`], but just the tag, no fields. Used in various kinds of matching.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AssocTag {
    Const,
    Fn,
    Type,
}

impl AssocTag {
    pub fn descr(self) -> &'static str {
        // This should match `DefKind::descr`.
        match self {
            Self::Const => "associated constant",
            Self::Fn => "associated function",
            Self::Type => "associated type",
        }
    }
}

/// A list of `ty::AssocItem`s in definition order that allows for efficient lookup by name.
///
/// When doing lookup by name, we try to postpone hygienic comparison for as long as possible since
/// it is relatively expensive. Instead, items are indexed by `Symbol` and hygienic comparison is
/// done only on items with the same name.
#[derive(Debug, Clone, PartialEq, StableHash)]
pub struct AssocItems {
    items: SortedIndexMultiMap<u32, Option<Symbol>, ty::AssocItem>,
}

impl AssocItems {
    /// Constructs an `AssociatedItems` map from a series of `ty::AssocItem`s in definition order.
    pub fn new(items_in_def_order: impl IntoIterator<Item = ty::AssocItem>) -> Self {
        let items = items_in_def_order.into_iter().map(|item| (item.opt_name(), item)).collect();
        AssocItems { items }
    }

    /// Returns an iterator over associated items in the order they were defined.
    ///
    /// New code should avoid relying on definition order. If you need a particular associated item
    /// for a known trait, make that trait a lang item instead of indexing this array.
    pub fn in_definition_order(&self) -> impl '_ + Iterator<Item = &ty::AssocItem> {
        self.items.iter().map(|(_, v)| v)
    }

    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// Returns an iterator over all associated items with the given name, ignoring hygiene.
    ///
    /// Panics if `name.is_empty()` returns `true`.
    pub fn filter_by_name_unhygienic(
        &self,
        name: Symbol,
    ) -> impl '_ + Iterator<Item = &ty::AssocItem> {
        assert!(!name.is_empty());
        self.items.get_by_key(Some(name))
    }

    /// Returns the associated item with the given identifier and `AssocKind`, if one exists.
    /// The identifier is ignoring hygiene. This is meant to be used for lints and diagnostics.
    pub fn filter_by_name_unhygienic_and_kind(
        &self,
        name: Symbol,
        assoc_tag: AssocTag,
    ) -> impl '_ + Iterator<Item = &ty::AssocItem> {
        self.filter_by_name_unhygienic(name).filter(move |item| item.tag() == assoc_tag)
    }

    /// Returns the associated item with the given identifier and `AssocKind`, if one exists.
    /// The identifier is matched hygienically.
    pub fn find_by_ident_and_kind(
        &self,
        tcx: TyCtxt<'_>,
        ident: Ident,
        assoc_tag: AssocTag,
        parent_def_id: DefId,
    ) -> Option<&ty::AssocItem> {
        self.filter_by_name_unhygienic(ident.name)
            .filter(|item| item.tag() == assoc_tag)
            .find(|item| tcx.hygienic_eq(ident, item.ident(tcx), parent_def_id))
    }

    /// Returns the associated item with the given identifier in the given `Namespace`, if one
    /// exists. The identifier is matched hygienically.
    pub fn find_by_ident_and_namespace(
        &self,
        tcx: TyCtxt<'_>,
        ident: Ident,
        ns: Namespace,
        parent_def_id: DefId,
    ) -> Option<&ty::AssocItem> {
        self.filter_by_name_unhygienic(ident.name)
            .filter(|item| item.namespace() == ns)
            .find(|item| tcx.hygienic_eq(ident, item.ident(tcx), parent_def_id))
    }
}

impl<'tcx> TyCtxt<'tcx> {
    /// Given an `fn_def_id` of a trait or a trait implementation:
    ///
    /// if `fn_def_id` is a function defined inside a trait, then it synthesizes
    /// a new def id corresponding to a new associated type for each return-
    /// position `impl Trait` in the signature.
    ///
    /// if `fn_def_id` is a function inside of an impl, then for each synthetic
    /// associated type generated for the corresponding trait function described
    /// above, synthesize a corresponding associated type in the impl.
    pub fn associated_types_for_impl_traits_in_associated_fn(
        self,
        fn_def_id: DefId,
    ) -> &'tcx [DefId] {
        let parent_def_id = self.parent(fn_def_id);
        &self.associated_types_for_impl_traits_in_trait_or_impl(parent_def_id)[&fn_def_id]
    }
}
