//! Miscellaneous type-system utilities that are too small to deserve their own modules.

use hir::LangItem;
use rustc_ast::Mutability;
use rustc_hir as hir;
use rustc_infer::infer::{RegionResolutionError, TyCtxtInferExt};
use rustc_middle::bug;
use rustc_middle::ty::{self, AdtDef, Ty, TyCtxt, TypeVisitableExt, TypingMode};
use rustc_span::{Span, sym};

use crate::regions::InferCtxtRegionExt;
use crate::traits::{self, FulfillmentError, Obligation, ObligationCause};

pub enum CopyImplementationError<'tcx> {
    InfringingFields(Vec<(&'tcx ty::FieldDef, Ty<'tcx>, InfringingFieldsReason<'tcx>)>),
    NotAnAdt,
    HasDestructor(hir::def_id::DefId),
    HasUnsafeFields,
}

pub enum ConstParamTyImplementationError<'tcx> {
    UnsizedConstParamsFeatureRequired,
    InvalidInnerTyOfBuiltinTy(Vec<(Ty<'tcx>, InfringingFieldsReason<'tcx>)>),
    InfrigingFields(Vec<(&'tcx ty::FieldDef, Ty<'tcx>, InfringingFieldsReason<'tcx>)>),
    NotAnAdtOrBuiltinAllowed,
    NonExhaustive(Span),
}

pub enum InfringingFieldsReason<'tcx> {
    Fulfill(Vec<FulfillmentError<'tcx>>),
    Regions(Vec<RegionResolutionError<'tcx>>),
}

/// Checks that the fields of the type (an ADT) all implement copy.
///
/// If fields don't implement copy, return an error containing a list of
/// those violating fields.
///
/// If it's not an ADT, int ty, `bool`, float ty, `char`, raw pointer, `!`,
/// a reference or an array returns `Err(NotAnAdt)`.
///
/// If the impl is `Safe`, `self_type` must not have unsafe fields. When used to
/// generate suggestions in lints, `Safe` should be supplied so as to not
/// suggest implementing `Copy` for types with unsafe fields.
pub fn type_allowed_to_implement_copy<'tcx>(
    tcx: TyCtxt<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    self_type: Ty<'tcx>,
    parent_cause: ObligationCause<'tcx>,
    impl_safety: hir::Safety,
) -> Result<(), CopyImplementationError<'tcx>> {
    let (adt, args) = match self_type.kind() {
        // These types used to have a builtin impl.
        // Now libcore provides that impl.
        ty::Uint(_)
        | ty::Int(_)
        | ty::Bool
        | ty::Float(_)
        | ty::Char
        | ty::RawPtr(..)
        | ty::Never
        | ty::Ref(_, _, hir::Mutability::Not)
        | ty::Array(..) => return Ok(()),

        &ty::Adt(adt, args) => (adt, args),

        _ => return Err(CopyImplementationError::NotAnAdt),
    };

    all_fields_implement_trait(
        tcx,
        param_env,
        self_type,
        adt,
        args,
        parent_cause,
        hir::LangItem::Copy,
    )
    .map_err(CopyImplementationError::InfringingFields)?;

    if let Some(did) = adt.destructor(tcx).map(|dtor| dtor.did) {
        return Err(CopyImplementationError::HasDestructor(did));
    }

    if impl_safety.is_safe() && self_type.has_unsafe_fields() {
        return Err(CopyImplementationError::HasUnsafeFields);
    }

    Ok(())
}

/// Checks that the fields of the type (an ADT) all implement `(Unsized?)ConstParamTy`.
///
/// If fields don't implement `(Unsized?)ConstParamTy`, return an error containing a list of
/// those violating fields.
///
/// If it's not an ADT, int ty, `bool` or `char`, returns `Err(NotAnAdtOrBuiltinAllowed)`.
pub fn type_allowed_to_implement_const_param_ty<'tcx>(
    tcx: TyCtxt<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    self_type: Ty<'tcx>,
    parent_cause: ObligationCause<'tcx>,
) -> Result<(), ConstParamTyImplementationError<'tcx>> {
    let mut need_unstable_feature_bound = false;

    let inner_tys: Vec<_> = match *self_type.kind() {
        // Trivially okay as these types are all:
        // - Sized
        // - Contain no nested types
        // - Have structural equality
        ty::Uint(_) | ty::Int(_) | ty::Bool | ty::Char => return Ok(()),

        // Handle types gated under `feature(unsized_const_params)`
        // FIXME(unsized_const_params): Make `const N: [u8]` work then forbid references
        ty::Slice(inner_ty) | ty::Ref(_, inner_ty, Mutability::Not) => {
            need_unstable_feature_bound = true;
            vec![inner_ty]
        }
        ty::Str => {
            need_unstable_feature_bound = true;
            vec![Ty::new_slice(tcx, tcx.types.u8)]
        }
        ty::Array(inner_ty, _) => vec![inner_ty],

        // `str` morally acts like a newtype around `[u8]`
        ty::Tuple(inner_tys) => inner_tys.into_iter().collect(),

        ty::Adt(adt, args) if adt.is_enum() || adt.is_struct() => {
            if !tcx.features().adt_const_params() {
                for variant in adt.variants() {
                    if variant.is_field_list_non_exhaustive() {
                        let attr_span = match hir::find_attr!(tcx, variant.def_id, hir::attrs::AttributeKind::NonExhaustive(span) => *span)
                        {
                            Some(sp) => sp,
                            None => bug!("non_exhaustive variant missing NonExhaustive attribute"),
                        };
                        return Err(ConstParamTyImplementationError::NonExhaustive(attr_span));
                    }
                }
            }

            all_fields_implement_trait(
                tcx,
                param_env,
                self_type,
                adt,
                args,
                parent_cause.clone(),
                LangItem::ConstParamTy,
            )
            .map_err(ConstParamTyImplementationError::InfrigingFields)?;

            vec![]
        }

        _ => return Err(ConstParamTyImplementationError::NotAnAdtOrBuiltinAllowed),
    };

    let mut infringing_inner_tys = vec![];
    for inner_ty in inner_tys {
        // We use an ocx per inner ty for better diagnostics
        let infcx = tcx.infer_ctxt().build(TypingMode::non_body_analysis());
        let ocx = traits::ObligationCtxt::new_with_diagnostics(&infcx);

        // Make sure impls certain types are gated with #[unstable_feature_bound(unsized_const_params)]
        if need_unstable_feature_bound {
            ocx.register_obligation(Obligation::new(
                tcx,
                parent_cause.clone(),
                param_env,
                ty::ClauseKind::UnstableFeature(sym::unsized_const_params),
            ));

            if !ocx.evaluate_obligations_error_on_ambiguity().is_empty() {
                return Err(ConstParamTyImplementationError::UnsizedConstParamsFeatureRequired);
            }
        }

        ocx.register_bound(
            parent_cause.clone(),
            param_env,
            inner_ty,
            tcx.require_lang_item(LangItem::ConstParamTy, parent_cause.span),
        );

        let errors = ocx.evaluate_obligations_error_on_ambiguity();
        if !errors.is_empty() {
            infringing_inner_tys.push((inner_ty, InfringingFieldsReason::Fulfill(errors)));
            continue;
        }

        // Check regions assuming the self type of the impl is WF
        let errors = infcx.resolve_regions(parent_cause.body_id, param_env, [self_type]);
        if !errors.is_empty() {
            infringing_inner_tys.push((inner_ty, InfringingFieldsReason::Regions(errors)));
            continue;
        }
    }

    if !infringing_inner_tys.is_empty() {
        return Err(ConstParamTyImplementationError::InvalidInnerTyOfBuiltinTy(
            infringing_inner_tys,
        ));
    }

    Ok(())
}

/// Check that all fields of a given `adt` implement `lang_item` trait.
pub fn all_fields_implement_trait<'tcx>(
    tcx: TyCtxt<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    self_type: Ty<'tcx>,
    adt: AdtDef<'tcx>,
    args: ty::GenericArgsRef<'tcx>,
    parent_cause: ObligationCause<'tcx>,
    lang_item: LangItem,
) -> Result<(), Vec<(&'tcx ty::FieldDef, Ty<'tcx>, InfringingFieldsReason<'tcx>)>> {
    let trait_def_id = tcx.require_lang_item(lang_item, parent_cause.span);

    let mut infringing = Vec::new();
    for variant in adt.variants() {
        for field in &variant.fields {
            // Do this per-field to get better error messages.
            let infcx = tcx.infer_ctxt().build(TypingMode::non_body_analysis());
            let ocx = traits::ObligationCtxt::new_with_diagnostics(&infcx);

            let unnormalized_ty = field.ty(tcx, args);
            if unnormalized_ty.references_error() {
                continue;
            }

            let field_span = tcx.def_span(field.did);
            let field_ty_span = match tcx.hir_get_if_local(field.did) {
                Some(hir::Node::Field(field_def)) => field_def.ty.span,
                _ => field_span,
            };

            // FIXME(compiler-errors): This gives us better spans for bad
            // projection types like in issue-50480.
            // If the ADT has args, point to the cause we are given.
            // If it does not, then this field probably doesn't normalize
            // to begin with, and point to the bad field's span instead.
            let normalization_cause = if field
                .ty(tcx, traits::GenericArgs::identity_for_item(tcx, adt.did()))
                .has_non_region_param()
            {
                parent_cause.clone()
            } else {
                ObligationCause::dummy_with_span(field_ty_span)
            };
            let ty: Ty<'_> = ocx.normalize(&normalization_cause, param_env, unnormalized_ty);
            let normalization_errors = ocx.try_evaluate_obligations();

            // NOTE: The post-normalization type may also reference errors,
            // such as when we project to a missing type or we have a mismatch
            // between expected and found const-generic types. Don't report an
            // additional copy error here, since it's not typically useful.
            if !normalization_errors.is_empty() || ty.references_error() {
                tcx.dcx().span_delayed_bug(
                    field_span,
                    format!(
                        "couldn't normalize struct field `{ty}` when checking {tr} implementation",
                        tr = tcx.def_path_str(trait_def_id)
                    ),
                );
                continue;
            }

            ocx.register_bound(
                ObligationCause::dummy_with_span(field_ty_span),
                param_env,
                ty,
                trait_def_id,
            );
            let errors = ocx.evaluate_obligations_error_on_ambiguity();
            if !errors.is_empty() {
                infringing.push((field, ty, InfringingFieldsReason::Fulfill(errors)));
            }

            // Check regions assuming the self type of the impl is WF
            let errors = infcx.resolve_regions(parent_cause.body_id, param_env, [self_type]);
            if !errors.is_empty() {
                infringing.push((field, ty, InfringingFieldsReason::Regions(errors)));
            }
        }
    }

    if infringing.is_empty() { Ok(()) } else { Err(infringing) }
}
