//! This module contains methods that assist in checking that impls are general
//! enough, i.e. that they always apply to every valid instantaiton of the ADT
//! they're implemented for.
//!
//! This is necessary for `Drop` and negative impls to be well-formed.

use rustc_data_structures::fx::FxHashSet;
use rustc_errors::codes::*;
use rustc_errors::{ErrorGuaranteed, struct_span_code_err};
use rustc_infer::infer::{RegionResolutionError, TyCtxtInferExt};
use rustc_infer::traits::{ObligationCause, ObligationCauseCode};
use rustc_middle::span_bug;
use rustc_middle::ty::util::CheckRegions;
use rustc_middle::ty::{self, GenericArgsRef, Ty, TyCtxt, TypeVisitableExt, TypingMode};
use rustc_span::sym;
use rustc_trait_selection::regions::InferCtxtRegionExt;
use rustc_trait_selection::traits::{self, ObligationCtxt};

use crate::errors;
use crate::hir::def_id::{DefId, LocalDefId};

/// This function confirms that the `Drop` implementation identified by
/// `drop_impl_did` is not any more specialized than the type it is
/// attached to (Issue #8142).
///
/// This means:
///
/// 1. The self type must be nominal (this is already checked during
///    coherence),
///
/// 2. The generic region/type parameters of the impl's self type must
///    all be parameters of the Drop impl itself (i.e., no
///    specialization like `impl Drop for Foo<i32>`), and,
///
/// 3. Any bounds on the generic parameters must be reflected in the
///    struct/enum definition for the nominal type itself (i.e.
///    cannot do `struct S<T>; impl<T:Clone> Drop for S<T> { ... }`).
pub(crate) fn check_drop_impl(
    tcx: TyCtxt<'_>,
    drop_impl_did: LocalDefId,
) -> Result<(), ErrorGuaranteed> {
    match tcx.impl_polarity(drop_impl_did) {
        ty::ImplPolarity::Positive => {}
        ty::ImplPolarity::Negative => {
            return Err(tcx.dcx().emit_err(errors::DropImplPolarity::Negative {
                span: tcx.def_span(drop_impl_did),
            }));
        }
        ty::ImplPolarity::Reservation => {
            return Err(tcx.dcx().emit_err(errors::DropImplPolarity::Reservation {
                span: tcx.def_span(drop_impl_did),
            }));
        }
    }

    tcx.ensure_result().orphan_check_impl(drop_impl_did)?;

    let self_ty = tcx.type_of(drop_impl_did).instantiate_identity().skip_norm_wip();

    match self_ty.kind() {
        ty::Adt(adt_def, adt_to_impl_args) => {
            ensure_impl_params_and_item_params_correspond(
                tcx,
                drop_impl_did,
                adt_def.did(),
                adt_to_impl_args,
            )?;

            ensure_all_fields_are_const_destruct(tcx, drop_impl_did, adt_def.did())?;

            ensure_impl_predicates_are_implied_by_item_defn(
                tcx,
                drop_impl_did,
                adt_def.did(),
                adt_to_impl_args,
            )?;

            check_drop_xor_pin_drop(tcx, adt_def.did(), drop_impl_did)?;

            Ok(())
        }
        _ => {
            span_bug!(tcx.def_span(drop_impl_did), "incoherent impl of Drop");
        }
    }
}

pub(crate) fn check_negative_auto_trait_impl<'tcx>(
    tcx: TyCtxt<'tcx>,
    impl_def_id: LocalDefId,
    impl_trait_ref: ty::TraitRef<'tcx>,
    polarity: ty::ImplPolarity,
) -> Result<(), ErrorGuaranteed> {
    let ty::ImplPolarity::Negative = polarity else {
        return Ok(());
    };

    if !tcx.trait_is_auto(impl_trait_ref.def_id) {
        return Ok(());
    }

    if tcx.defaultness(impl_def_id).is_default() {
        tcx.dcx().span_delayed_bug(tcx.def_span(impl_def_id), "default impl cannot be negative");
    }

    tcx.ensure_result().orphan_check_impl(impl_def_id)?;

    match impl_trait_ref.self_ty().kind() {
        ty::Adt(adt_def, adt_to_impl_args) => {
            ensure_impl_params_and_item_params_correspond(
                tcx,
                impl_def_id,
                adt_def.did(),
                adt_to_impl_args,
            )?;

            ensure_impl_predicates_are_implied_by_item_defn(
                tcx,
                impl_def_id,
                adt_def.did(),
                adt_to_impl_args,
            )
        }
        _ => {
            if tcx.features().auto_traits() {
                // NOTE: We ignore the applicability check for negative auto impls
                // defined in libcore. In the (almost impossible) future where we
                // stabilize auto impls, then the proper applicability check MUST
                // be implemented here to handle non-ADT rigid types.
                Ok(())
            } else {
                Err(tcx.dcx().span_delayed_bug(
                    tcx.def_span(impl_def_id),
                    "incoherent impl of negative auto trait",
                ))
            }
        }
    }
}

fn ensure_impl_params_and_item_params_correspond<'tcx>(
    tcx: TyCtxt<'tcx>,
    impl_def_id: LocalDefId,
    adt_def_id: DefId,
    adt_to_impl_args: GenericArgsRef<'tcx>,
) -> Result<(), ErrorGuaranteed> {
    let Err(arg) = tcx.uses_unique_generic_params(adt_to_impl_args, CheckRegions::OnlyParam) else {
        return Ok(());
    };

    let impl_span = tcx.def_span(impl_def_id);
    let item_span = tcx.def_span(adt_def_id);
    let self_descr = tcx.def_descr(adt_def_id);
    let polarity = match tcx.impl_polarity(impl_def_id) {
        ty::ImplPolarity::Positive | ty::ImplPolarity::Reservation => "",
        ty::ImplPolarity::Negative => "!",
    };
    let trait_name = tcx.item_name(tcx.impl_trait_id(impl_def_id.to_def_id()));
    let mut err = struct_span_code_err!(
        tcx.dcx(),
        impl_span,
        E0366,
        "`{polarity}{trait_name}` impls cannot be specialized",
    );
    match arg {
        ty::util::NotUniqueParam::DuplicateParam(arg) => {
            err.note(format!("`{arg}` is mentioned multiple times"))
        }
        ty::util::NotUniqueParam::NotParam(arg) => {
            err.note(format!("`{arg}` is not a generic parameter"))
        }
    };
    err.span_note(
        item_span,
        format!(
            "use the same sequence of generic lifetime, type and const parameters \
                     as the {self_descr} definition",
        ),
    );
    Err(err.emit())
}

fn ensure_all_fields_are_const_destruct<'tcx>(
    tcx: TyCtxt<'tcx>,
    impl_def_id: LocalDefId,
    adt_def_id: DefId,
) -> Result<(), ErrorGuaranteed> {
    if !tcx.is_conditionally_const(impl_def_id) {
        return Ok(());
    }
    let infcx = tcx.infer_ctxt().build(TypingMode::non_body_analysis());
    let ocx = ObligationCtxt::new_with_diagnostics(&infcx);

    let impl_span = tcx.def_span(impl_def_id.to_def_id());
    let env =
        ty::EarlyBinder::bind(tcx.param_env(impl_def_id)).instantiate_identity().skip_norm_wip();
    let args = ty::GenericArgs::identity_for_item(tcx, impl_def_id);
    let destruct_trait = tcx.lang_items().destruct_trait().unwrap();
    for field in tcx.adt_def(adt_def_id).all_fields() {
        let field_ty = field.ty(tcx, args).skip_norm_wip();
        let cause = traits::ObligationCause::new(
            tcx.def_span(field.did),
            impl_def_id,
            ObligationCauseCode::Misc,
        );
        ocx.register_obligation(traits::Obligation::new(
            tcx,
            cause,
            env,
            ty::ClauseKind::HostEffect(ty::HostEffectPredicate {
                trait_ref: ty::TraitRef::new(tcx, destruct_trait, [field_ty]),
                constness: ty::BoundConstness::Maybe,
            }),
        ));
    }
    ocx.evaluate_obligations_error_on_ambiguity()
        .into_iter()
        .map(|error| {
            let ty::ClauseKind::HostEffect(eff) =
                error.root_obligation.predicate.expect_clause().kind().no_bound_vars().unwrap()
            else {
                unreachable!()
            };
            let field_ty = eff.trait_ref.self_ty();
            let mut diag = struct_span_code_err!(
                tcx.dcx(),
                error.root_obligation.cause.span,
                E0367,
                "`{field_ty}` does not implement `[const] Destruct`",
            )
            .with_span_note(impl_span, "required for this `Drop` impl");
            if field_ty.has_param()
                && let Some(generics) = tcx.hir_node_by_def_id(impl_def_id).generics()
            {
                let destruct_def_id = tcx.lang_items().destruct_trait();
                ty::suggest_constraining_type_param(
                    tcx,
                    generics,
                    &mut diag,
                    &field_ty.to_string(),
                    "[const] Destruct",
                    destruct_def_id,
                    None,
                );
            }
            Err(diag.emit())
        })
        .collect()
}

/// Confirms that all predicates defined on the `Drop` impl (`drop_impl_def_id`) are able to be
/// proven from within `adt_def_id`'s environment. I.e. all the predicates on the impl are
/// implied by the ADT being well formed.
fn ensure_impl_predicates_are_implied_by_item_defn<'tcx>(
    tcx: TyCtxt<'tcx>,
    impl_def_id: LocalDefId,
    adt_def_id: DefId,
    adt_to_impl_args: GenericArgsRef<'tcx>,
) -> Result<(), ErrorGuaranteed> {
    let infcx = tcx.infer_ctxt().build(TypingMode::non_body_analysis());
    let ocx = ObligationCtxt::new_with_diagnostics(&infcx);

    let impl_span = tcx.def_span(impl_def_id.to_def_id());
    let trait_name = tcx.item_name(tcx.impl_trait_id(impl_def_id.to_def_id()));
    let polarity = match tcx.impl_polarity(impl_def_id) {
        ty::ImplPolarity::Positive | ty::ImplPolarity::Reservation => "",
        ty::ImplPolarity::Negative => "!",
    };
    // Take the param-env of the adt and instantiate the args that show up in
    // the implementation's self type. This gives us the assumptions that the
    // self ty of the implementation is allowed to know just from it being a
    // well-formed adt, since that's all we're allowed to assume while proving
    // the Drop implementation is not specialized.
    //
    // We don't need to normalize this param-env or anything, since we're only
    // instantiating it with free params, so no additional param-env normalization
    // can occur on top of what has been done in the param_env query itself.
    //
    // Note: Ideally instead of instantiating the `ParamEnv` with the arguments from the impl ty we
    // could instead use identity args for the adt. Unfortunately this would cause any errors to
    // reference the params from the ADT instead of from the impl which is bad UX. To resolve
    // this we "rename" the ADT's params to be the impl's params which should not affect behaviour.
    let impl_adt_ty = Ty::new_adt(tcx, tcx.adt_def(adt_def_id), adt_to_impl_args);
    let adt_env = ty::EarlyBinder::bind(tcx.param_env(adt_def_id))
        .instantiate(tcx, adt_to_impl_args)
        .skip_norm_wip();

    let fresh_impl_args = infcx.fresh_args_for_item(impl_span, impl_def_id.to_def_id());
    let fresh_adt_ty =
        tcx.impl_trait_ref(impl_def_id).instantiate(tcx, fresh_impl_args).skip_norm_wip().self_ty();

    ocx.eq(&ObligationCause::dummy_with_span(impl_span), adt_env, fresh_adt_ty, impl_adt_ty)
        .expect("equating fully generic trait ref should never fail");

    for (clause, span) in tcx.predicates_of(impl_def_id).instantiate(tcx, fresh_impl_args) {
        let normalize_cause = traits::ObligationCause::misc(span, impl_def_id);
        let pred = ocx.normalize(&normalize_cause, adt_env, clause);
        let cause = traits::ObligationCause::new(
            span,
            impl_def_id,
            ObligationCauseCode::AlwaysApplicableImpl,
        );
        ocx.register_obligation(traits::Obligation::new(tcx, cause, adt_env, pred));
    }

    // All of the custom error reporting logic is to preserve parity with the old
    // error messages.
    //
    // They can probably get removed with better treatment of the new `DropImpl`
    // obligation cause code, and perhaps some custom logic in `report_region_errors`.

    let errors = ocx.evaluate_obligations_error_on_ambiguity();
    if !errors.is_empty() {
        let mut guar = None;
        let mut root_predicates = FxHashSet::default();
        for error in errors {
            let root_predicate = error.root_obligation.predicate;
            if root_predicates.insert(root_predicate) {
                let item_span = tcx.def_span(adt_def_id);
                let self_descr = tcx.def_descr(adt_def_id);
                guar = Some(
                    struct_span_code_err!(
                        tcx.dcx(),
                        error.root_obligation.cause.span,
                        E0367,
                        "`{polarity}{trait_name}` impl requires `{root_predicate}` \
                        but the {self_descr} it is implemented for does not",
                    )
                    .with_span_note(item_span, "the implementor must specify the same requirement")
                    .emit(),
                );
            }
        }
        return Err(guar.unwrap());
    }

    let errors = ocx.infcx.resolve_regions(impl_def_id, adt_env, []);
    if !errors.is_empty() {
        let mut guar = None;
        for error in errors {
            let item_span = tcx.def_span(adt_def_id);
            let self_descr = tcx.def_descr(adt_def_id);
            let outlives = match error {
                RegionResolutionError::ConcreteFailure(_, a, b) => format!("{b}: {a}"),
                RegionResolutionError::GenericBoundFailure(_, generic, r) => {
                    format!("{generic}: {r}")
                }
                RegionResolutionError::SubSupConflict(_, _, _, a, _, b, _) => format!("{b}: {a}"),
                RegionResolutionError::UpperBoundUniverseConflict(a, _, _, _, b) => {
                    format!("{b}: {a}", a = ty::Region::new_var(tcx, a))
                }
                RegionResolutionError::CannotNormalize(..) => unreachable!(),
            };
            guar = Some(
                struct_span_code_err!(
                    tcx.dcx(),
                    error.origin().span(),
                    E0367,
                    "`{polarity}{trait_name}` impl requires `{outlives}` \
                    but the {self_descr} it is implemented for does not",
                )
                .with_span_note(item_span, "the implementor must specify the same requirement")
                .emit(),
            );
        }
        return Err(guar.unwrap());
    }

    Ok(())
}

/// This function checks at least and at most one of `Drop::drop` and `Drop::pin_drop` is implemented.
/// It also checks that `Drop::pin_drop` must be implemented if `#[pin_v2]` is present on the type.
fn check_drop_xor_pin_drop<'tcx>(
    tcx: TyCtxt<'tcx>,
    adt_def_id: DefId,
    drop_impl_did: LocalDefId,
) -> Result<(), ErrorGuaranteed> {
    let mut drop_span = None;
    let mut pin_drop_span = None;
    for item in tcx.associated_items(drop_impl_did).in_definition_order() {
        match item.kind {
            ty::AssocKind::Fn { name: sym::drop, .. } => {
                drop_span = Some(tcx.def_span(item.def_id))
            }
            ty::AssocKind::Fn { name: sym::pin_drop, .. } => {
                pin_drop_span = Some(tcx.def_span(item.def_id))
            }
            _ => {}
        }
    }

    match (drop_span, pin_drop_span) {
        (None, None) => {
            if tcx.features().pin_ergonomics() {
                return Err(tcx.dcx().emit_err(crate::errors::MissingOneOfTraitItem {
                    span: tcx.def_span(drop_impl_did),
                    note: None,
                    missing_items_msg: "drop`, `pin_drop".to_string(),
                }));
            } else {
                return Err(tcx
                    .dcx()
                    .span_delayed_bug(tcx.def_span(drop_impl_did), "missing `Drop::drop`"));
            }
        }
        (Some(span), None) => {
            if tcx.adt_def(adt_def_id).is_pin_project() {
                let pin_v2_span = rustc_hir::find_attr!(tcx, adt_def_id, PinV2(attr) => *attr);
                let adt_name = tcx.item_name(adt_def_id);
                return Err(tcx.dcx().emit_err(crate::errors::PinV2WithoutPinDrop {
                    span,
                    pin_v2_span,
                    adt_name,
                }));
            }
        }
        (None, Some(span)) => {
            if !tcx.features().pin_ergonomics() {
                return Err(tcx.dcx().span_delayed_bug(
                    span,
                    "`Drop::pin_drop` should be guarded by the library feature gate",
                ));
            }
        }
        (Some(drop_span), Some(pin_drop_span)) => {
            return Err(tcx.dcx().emit_err(crate::errors::ConflictImplDropAndPinDrop {
                span: tcx.def_span(drop_impl_did),
                drop_span,
                pin_drop_span,
            }));
        }
    }
    Ok(())
}
