use rustc_hir::def_id::{DefId, LocalDefId};
use rustc_hir::{
    Constness, ExprKind, ForeignItemKind, ImplItem, ImplItemImplKind, ImplItemKind, Item, ItemKind,
    Node, TraitItem, TraitItemKind, VariantData, find_attr,
};
use rustc_middle::query::Providers;
use rustc_middle::ty::TyCtxt;

/// Checks whether a function-like definition is considered to be `const`. Also stores constness of inherent impls.
fn constness(tcx: TyCtxt<'_>, def_id: LocalDefId) -> Constness {
    let node = tcx.hir_node_by_def_id(def_id);

    match node {
        Node::Ctor(VariantData::Tuple(..)) => Constness::Const { always: false },
        Node::ForeignItem(item) if let ForeignItemKind::Fn(..) = item.kind => {
            // Foreign functions cannot be evaluated at compile-time.
            Constness::NotConst
        }
        Node::Expr(e) if let ExprKind::Closure(c) = e.kind => {
            if let Constness::Const { .. } = c.constness && tcx.hir_body_const_context(tcx.local_parent(def_id)).is_none() {
                tcx.dcx().span_err(tcx.def_span(def_id), "cannot use `const` closures outside of const contexts");
                return Constness::NotConst;
            }
            c.constness
        },
        // FIXME(fee1-dead): extract this one out and rename this query to `fn_constness` so we don't need `is_const_fn` anymore.
        Node::Item(i) if let ItemKind::Impl(impl_) = i.kind => impl_.constness,
        Node::Item(Item { kind: ItemKind::Fn { sig, .. }, .. }) => sig.header.constness,
        Node::ImplItem(ImplItem {
            impl_kind: ImplItemImplKind::Trait { .. },
            kind: ImplItemKind::Fn(..),
            ..
        }) => tcx.impl_trait_header(tcx.local_parent(def_id)).constness,
        Node::ImplItem(ImplItem {
            impl_kind: ImplItemImplKind::Inherent { .. },
            kind: ImplItemKind::Fn(sig, _),
            ..
        }) => {
            match sig.header.constness {
                Constness::Const { always } => Constness::Const { always },
                // inherent impl could be const
                Constness::NotConst => tcx.constness(tcx.local_parent(def_id)),
            }
        }
        Node::TraitItem(ti @ TraitItem { kind: TraitItemKind::Fn(..), .. }) => {
            if find_attr!(tcx, ti.hir_id(), RustcNonConstTraitMethod) {
                Constness::NotConst
            } else {
                tcx.trait_def(tcx.local_parent(def_id)).constness
            }
        }
        _ => {
            tcx.dcx().span_bug(
                tcx.def_span(def_id),
                format!("should not be requesting the constness of items that can't be const: {node:#?}: {:?}", tcx.def_kind(def_id))
            )
        }
    }
}

fn is_promotable_const_fn(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
    tcx.is_const_fn(def_id)
        && match tcx.lookup_const_stability(def_id) {
            Some(stab) => {
                if cfg!(debug_assertions) && stab.promotable {
                    let sig = tcx.fn_sig(def_id);
                    assert!(
                        sig.skip_binder().safety().is_safe(),
                        "don't mark const unsafe fns as promotable",
                        // https://github.com/rust-lang/rust/pull/53851#issuecomment-418760682
                    );
                }
                stab.promotable
            }
            None => false,
        }
}

pub fn provide(providers: &mut Providers) {
    *providers = Providers { constness, is_promotable_const_fn, ..*providers };
}
