//! Deeply normalize types using the old trait solver.

use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_errors::msg;
use rustc_hir::def::DefKind;
use rustc_infer::infer::at::At;
use rustc_infer::infer::{InferCtxt, InferOk};
use rustc_infer::traits::{
    FromSolverError, Normalized, Obligation, PredicateObligations, TraitEngine,
};
use rustc_macros::extension;
use rustc_middle::span_bug;
use rustc_middle::traits::{ObligationCause, ObligationCauseCode};
use rustc_middle::ty::{
    self, AliasTerm, Term, Ty, TyCtxt, TypeFoldable, TypeFolder, TypeSuperFoldable, TypeVisitable,
    TypeVisitableExt, TypingMode,
};
use tracing::{debug, instrument};

use super::{BoundVarReplacer, PlaceholderReplacer, SelectionContext, project};
use crate::error_reporting::InferCtxtErrorExt;
use crate::error_reporting::traits::OverflowCause;
use crate::solve::NextSolverError;

#[extension(pub trait NormalizeExt<'tcx>)]
impl<'tcx> At<'_, 'tcx> {
    /// Normalize a value using the `AssocTypeNormalizer`.
    ///
    /// This normalization should be used when the type contains inference variables or the
    /// projection may be fallible.
    fn normalize<T: TypeFoldable<TyCtxt<'tcx>>>(&self, value: T) -> InferOk<'tcx, T> {
        if self.infcx.next_trait_solver() {
            InferOk { value, obligations: PredicateObligations::new() }
        } else {
            let mut selcx = SelectionContext::new(self.infcx);
            let Normalized { value, obligations } =
                normalize_with_depth(&mut selcx, self.param_env, self.cause.clone(), 0, value);
            InferOk { value, obligations }
        }
    }

    /// Deeply normalizes `value`, replacing all aliases which can by normalized in
    /// the current environment. In the new solver this errors in case normalization
    /// fails or is ambiguous.
    ///
    /// In the old solver this simply uses `normalizes` and adds the nested obligations
    /// to the `fulfill_cx`. This is necessary as we otherwise end up recomputing the
    /// same goals in both a temporary and the shared context which negatively impacts
    /// performance as these don't share caching.
    ///
    /// FIXME(-Znext-solver=no): For performance reasons, we currently reuse an existing
    /// fulfillment context in the old solver. Once we have removed the old solver, we
    /// can remove the `fulfill_cx` parameter on this function.
    fn deeply_normalize<T, E>(
        self,
        value: T,
        fulfill_cx: &mut dyn TraitEngine<'tcx, E>,
    ) -> Result<T, Vec<E>>
    where
        T: TypeFoldable<TyCtxt<'tcx>>,
        E: FromSolverError<'tcx, NextSolverError<'tcx>>,
    {
        if self.infcx.next_trait_solver() {
            crate::solve::deeply_normalize(self, value)
        } else {
            if fulfill_cx.has_pending_obligations() {
                let pending_obligations = fulfill_cx.pending_obligations();
                span_bug!(
                    pending_obligations[0].cause.span,
                    "deeply_normalize should not be called with pending obligations: \
                    {pending_obligations:#?}"
                );
            }
            let value = self
                .normalize(value)
                .into_value_registering_obligations(self.infcx, &mut *fulfill_cx);
            let errors = fulfill_cx.evaluate_obligations_error_on_ambiguity(self.infcx);
            let value = self.infcx.resolve_vars_if_possible(value);
            if errors.is_empty() {
                Ok(value)
            } else {
                // Drop pending obligations, since deep normalization may happen
                // in a loop and we don't want to trigger the assertion on the next
                // iteration due to pending ambiguous obligations we've left over.
                let _ = fulfill_cx.collect_remaining_errors(self.infcx);
                Err(errors)
            }
        }
    }
}

/// As `normalize`, but with a custom depth.
pub(crate) fn normalize_with_depth<'a, 'b, 'tcx, T>(
    selcx: &'a mut SelectionContext<'b, 'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    cause: ObligationCause<'tcx>,
    depth: usize,
    value: T,
) -> Normalized<'tcx, T>
where
    T: TypeFoldable<TyCtxt<'tcx>>,
{
    let mut obligations = PredicateObligations::new();
    let value = normalize_with_depth_to(selcx, param_env, cause, depth, value, &mut obligations);
    Normalized { value, obligations }
}

#[instrument(level = "info", skip(selcx, param_env, cause, obligations))]
pub(crate) fn normalize_with_depth_to<'a, 'b, 'tcx, T>(
    selcx: &'a mut SelectionContext<'b, 'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    cause: ObligationCause<'tcx>,
    depth: usize,
    value: T,
    obligations: &mut PredicateObligations<'tcx>,
) -> T
where
    T: TypeFoldable<TyCtxt<'tcx>>,
{
    debug!(obligations.len = obligations.len());
    let mut normalizer = AssocTypeNormalizer::new(selcx, param_env, cause, depth, obligations);
    let result = ensure_sufficient_stack(|| AssocTypeNormalizer::fold(&mut normalizer, value));
    debug!(?result, obligations.len = normalizer.obligations.len());
    debug!(?normalizer.obligations,);
    result
}

pub(super) fn needs_normalization<'tcx, T: TypeVisitable<TyCtxt<'tcx>>>(
    infcx: &InferCtxt<'tcx>,
    value: &T,
) -> bool {
    let mut flags = ty::TypeFlags::HAS_ALIAS;

    // Opaques are treated as rigid outside of `TypingMode::PostAnalysis`,
    // so we can ignore those.
    match infcx.typing_mode() {
        // FIXME(#132279): We likely want to reveal opaques during post borrowck analysis
        TypingMode::Coherence
        | TypingMode::Analysis { .. }
        | TypingMode::Borrowck { .. }
        | TypingMode::PostBorrowckAnalysis { .. } => flags.remove(ty::TypeFlags::HAS_TY_OPAQUE),
        TypingMode::PostAnalysis => {}
    }

    value.has_type_flags(flags)
}

struct AssocTypeNormalizer<'a, 'b, 'tcx> {
    selcx: &'a mut SelectionContext<'b, 'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    cause: ObligationCause<'tcx>,
    obligations: &'a mut PredicateObligations<'tcx>,
    depth: usize,
    universes: Vec<Option<ty::UniverseIndex>>,
}

impl<'a, 'b, 'tcx> AssocTypeNormalizer<'a, 'b, 'tcx> {
    fn new(
        selcx: &'a mut SelectionContext<'b, 'tcx>,
        param_env: ty::ParamEnv<'tcx>,
        cause: ObligationCause<'tcx>,
        depth: usize,
        obligations: &'a mut PredicateObligations<'tcx>,
    ) -> AssocTypeNormalizer<'a, 'b, 'tcx> {
        debug_assert!(!selcx.infcx.next_trait_solver());
        AssocTypeNormalizer { selcx, param_env, cause, obligations, depth, universes: vec![] }
    }

    fn fold<T: TypeFoldable<TyCtxt<'tcx>>>(&mut self, value: T) -> T {
        let value = self.selcx.infcx.resolve_vars_if_possible(value);
        debug!(?value);

        assert!(
            !value.has_escaping_bound_vars(),
            "Normalizing {value:?} without wrapping in a `Binder`"
        );

        if !needs_normalization(self.selcx.infcx, &value) { value } else { value.fold_with(self) }
    }

    // FIXME(mgca): While this supports constants, it is only used for types by default right now
    #[instrument(level = "debug", skip(self), ret)]
    fn normalize_trait_projection(&mut self, proj: AliasTerm<'tcx>) -> Term<'tcx> {
        if !proj.has_escaping_bound_vars() {
            // When we don't have escaping bound vars we can normalize ambig aliases
            // to inference variables (done in `normalize_projection_ty`). This would
            // be wrong if there were escaping bound vars as even if we instantiated
            // the bound vars with placeholders, we wouldn't be able to map them back
            // after normalization succeeded.
            //
            // Also, as an optimization: when we don't have escaping bound vars, we don't
            // need to replace them with placeholders (see branch below).
            let proj = proj.fold_with(self);
            project::normalize_projection_term(
                self.selcx,
                self.param_env,
                proj,
                self.cause.clone(),
                self.depth,
                self.obligations,
            )
        } else {
            // If there are escaping bound vars, we temporarily replace the
            // bound vars with placeholders. Note though, that in the case
            // that we still can't project for whatever reason (e.g. self
            // type isn't known enough), we *can't* register an obligation
            // and return an inference variable (since then that obligation
            // would have bound vars and that's a can of worms). Instead,
            // we just give up and fall back to pretending like we never tried!
            //
            // Note: this isn't necessarily the final approach here; we may
            // want to figure out how to register obligations with escaping vars
            // or handle this some other way.
            let infcx = self.selcx.infcx;
            let (proj, mapped_regions, mapped_types, mapped_consts) =
                BoundVarReplacer::replace_bound_vars(infcx, &mut self.universes, proj);
            let proj = proj.fold_with(self);
            let normalized_term = project::opt_normalize_projection_term(
                self.selcx,
                self.param_env,
                proj,
                self.cause.clone(),
                self.depth,
                self.obligations,
            )
            .ok()
            .flatten()
            .unwrap_or_else(|| proj.to_term(infcx.tcx));

            PlaceholderReplacer::replace_placeholders(
                infcx,
                mapped_regions,
                mapped_types,
                mapped_consts,
                &self.universes,
                normalized_term,
            )
        }
    }

    // FIXME(mgca): While this supports constants, it is only used for types by default right now
    #[instrument(level = "debug", skip(self), ret)]
    fn normalize_inherent_projection(&mut self, inherent: AliasTerm<'tcx>) -> Term<'tcx> {
        if !inherent.has_escaping_bound_vars() {
            // When we don't have escaping bound vars we can normalize ambig aliases
            // to inference variables (done in `normalize_projection_ty`). This would
            // be wrong if there were escaping bound vars as even if we instantiated
            // the bound vars with placeholders, we wouldn't be able to map them back
            // after normalization succeeded.
            //
            // Also, as an optimization: when we don't have escaping bound vars, we don't
            // need to replace them with placeholders (see branch below).

            let inherent = inherent.fold_with(self);
            project::normalize_inherent_projection(
                self.selcx,
                self.param_env,
                inherent,
                self.cause.clone(),
                self.depth,
                self.obligations,
            )
        } else {
            let infcx = self.selcx.infcx;
            let (inherent, mapped_regions, mapped_types, mapped_consts) =
                BoundVarReplacer::replace_bound_vars(infcx, &mut self.universes, inherent);
            let inherent = inherent.fold_with(self);
            let inherent = project::normalize_inherent_projection(
                self.selcx,
                self.param_env,
                inherent,
                self.cause.clone(),
                self.depth,
                self.obligations,
            );

            PlaceholderReplacer::replace_placeholders(
                infcx,
                mapped_regions,
                mapped_types,
                mapped_consts,
                &self.universes,
                inherent,
            )
        }
    }

    // FIXME(mgca): While this supports constants, it is only used for types by default right now
    #[instrument(level = "debug", skip(self), ret)]
    fn normalize_free_alias(&mut self, free: AliasTerm<'tcx>) -> Term<'tcx> {
        let recursion_limit = self.cx().recursion_limit();
        if !recursion_limit.value_within_limit(self.depth) {
            self.selcx.infcx.err_ctxt().report_overflow_error(
                OverflowCause::DeeplyNormalize(free.into()),
                self.cause.span,
                false,
                |diag| {
                    diag.note(msg!("in case this is a recursive type alias, consider using a struct, enum, or union instead"));
                },
            );
        }

        // We don't replace bound vars in the generic arguments of the free alias with
        // placeholders. This doesn't cause any issues as instantiating parameters with
        // bound variables is special-cased to rewrite the debruijn index to be higher
        // whenever we fold through a binder.
        //
        // However, we do replace any escaping bound vars in the resulting goals with
        // placeholders as the trait solver does not expect to encounter escaping bound
        // vars in obligations.
        //
        // FIXME(lazy_type_alias): Check how much this actually matters for perf before
        // stabilization. This is a bit weird and generally not how we handle binders in
        // the compiler so ideally we'd do the same boundvar->placeholder->boundvar dance
        // that other kinds of normalization do.
        let infcx = self.selcx.infcx;
        self.obligations.extend(
            infcx.tcx.predicates_of(free.def_id).instantiate_own(infcx.tcx, free.args).map(
                |(mut predicate, span)| {
                    if free.has_escaping_bound_vars() {
                        (predicate, ..) = BoundVarReplacer::replace_bound_vars(
                            infcx,
                            &mut self.universes,
                            predicate,
                        );
                    }
                    let mut cause = self.cause.clone();
                    cause.map_code(|code| ObligationCauseCode::TypeAlias(code, span, free.def_id));
                    Obligation::new(infcx.tcx, cause, self.param_env, predicate)
                },
            ),
        );
        self.depth += 1;
        let res = if free.kind(infcx.tcx).is_type() {
            infcx.tcx.type_of(free.def_id).instantiate(infcx.tcx, free.args).fold_with(self).into()
        } else {
            infcx
                .tcx
                .const_of_item(free.def_id)
                .instantiate(infcx.tcx, free.args)
                .fold_with(self)
                .into()
        };
        self.depth -= 1;
        res
    }
}

impl<'a, 'b, 'tcx> TypeFolder<TyCtxt<'tcx>> for AssocTypeNormalizer<'a, 'b, 'tcx> {
    fn cx(&self) -> TyCtxt<'tcx> {
        self.selcx.tcx()
    }

    fn fold_binder<T: TypeFoldable<TyCtxt<'tcx>>>(
        &mut self,
        t: ty::Binder<'tcx, T>,
    ) -> ty::Binder<'tcx, T> {
        self.universes.push(None);
        let t = t.super_fold_with(self);
        self.universes.pop();
        t
    }

    fn fold_ty(&mut self, ty: Ty<'tcx>) -> Ty<'tcx> {
        if !needs_normalization(self.selcx.infcx, &ty) {
            return ty;
        }

        let ty::Alias(data) = *ty.kind() else { return ty.super_fold_with(self) };

        // We try to be a little clever here as a performance optimization in
        // cases where there are nested projections under binders.
        // For example:
        // ```
        // for<'a> fn(<T as Foo>::One<'a, Box<dyn Bar<'a, Item=<T as Foo>::Two<'a>>>>)
        // ```
        // We normalize the args on the projection before the projecting, but
        // if we're naive, we'll
        //   replace bound vars on inner, project inner, replace placeholders on inner,
        //   replace bound vars on outer, project outer, replace placeholders on outer
        //
        // However, if we're a bit more clever, we can replace the bound vars
        // on the entire type before normalizing nested projections, meaning we
        //   replace bound vars on outer, project inner,
        //   project outer, replace placeholders on outer
        //
        // This is possible because the inner `'a` will already be a placeholder
        // when we need to normalize the inner projection
        //
        // On the other hand, this does add a bit of complexity, since we only
        // replace bound vars if the current type is a `Projection` and we need
        // to make sure we don't forget to fold the args regardless.

        match data.kind {
            ty::Opaque { def_id } => {
                // Only normalize `impl Trait` outside of type inference, usually in codegen.
                match self.selcx.infcx.typing_mode() {
                    // FIXME(#132279): We likely want to reveal opaques during post borrowck analysis
                    TypingMode::Coherence
                    | TypingMode::Analysis { .. }
                    | TypingMode::Borrowck { .. }
                    | TypingMode::PostBorrowckAnalysis { .. } => ty.super_fold_with(self),
                    TypingMode::PostAnalysis => {
                        let recursion_limit = self.cx().recursion_limit();
                        if !recursion_limit.value_within_limit(self.depth) {
                            self.selcx.infcx.err_ctxt().report_overflow_error(
                                OverflowCause::DeeplyNormalize(data.into()),
                                self.cause.span,
                                true,
                                |_| {},
                            );
                        }

                        let args = data.args.fold_with(self);
                        let generic_ty = self.cx().type_of(def_id);
                        let concrete_ty = generic_ty.instantiate(self.cx(), args);
                        self.depth += 1;
                        let folded_ty = self.fold_ty(concrete_ty);
                        self.depth -= 1;
                        folded_ty
                    }
                }
            }

            ty::Projection { .. } => self.normalize_trait_projection(data.into()).expect_type(),
            ty::Inherent { .. } => self.normalize_inherent_projection(data.into()).expect_type(),
            ty::Free { .. } => self.normalize_free_alias(data.into()).expect_type(),
        }
    }

    #[instrument(skip(self), level = "debug")]
    fn fold_const(&mut self, ct: ty::Const<'tcx>) -> ty::Const<'tcx> {
        let tcx = self.selcx.tcx();

        if tcx.features().generic_const_exprs()
            // Normalize type_const items even with feature `generic_const_exprs`.
            && !matches!(ct.kind(), ty::ConstKind::Unevaluated(uv) if tcx.is_type_const(uv.def))
            || !needs_normalization(self.selcx.infcx, &ct)
        {
            return ct;
        }

        let uv = match ct.kind() {
            ty::ConstKind::Unevaluated(uv) => uv,
            _ => return ct.super_fold_with(self),
        };

        // Note that the AssocConst and Const cases are unreachable on stable,
        // unless a `min_generic_const_args` feature gate error has already
        // been emitted earlier in compilation.
        //
        // That's because we can only end up with an Unevaluated ty::Const for a const item
        // if it was marked with `type const`. Using this attribute without the mgca
        // feature gate causes a parse error.
        let ct = match tcx.def_kind(uv.def) {
            DefKind::AssocConst { .. } => match tcx.def_kind(tcx.parent(uv.def)) {
                DefKind::Trait => self.normalize_trait_projection(uv.into()).expect_const(),
                DefKind::Impl { of_trait: false } => {
                    self.normalize_inherent_projection(uv.into()).expect_const()
                }
                kind => unreachable!(
                    "unexpected `DefKind` for const alias' resolution's parent def: {:?}",
                    kind
                ),
            },
            DefKind::Const { .. } => self.normalize_free_alias(uv.into()).expect_const(),
            DefKind::AnonConst => {
                let ct = ct.super_fold_with(self);
                super::with_replaced_escaping_bound_vars(
                    self.selcx.infcx,
                    &mut self.universes,
                    ct,
                    |ct| super::evaluate_const(self.selcx.infcx, ct, self.param_env),
                )
            }
            kind => {
                unreachable!("unexpected `DefKind` for const alias to resolve to: {:?}", kind)
            }
        };

        // We re-fold the normalized const as the `ty` field on `ConstKind::Value` may be
        // unnormalized after const evaluation returns.
        ct.super_fold_with(self)
    }

    #[inline]
    fn fold_predicate(&mut self, p: ty::Predicate<'tcx>) -> ty::Predicate<'tcx> {
        if p.allow_normalization() && needs_normalization(self.selcx.infcx, &p) {
            p.super_fold_with(self)
        } else {
            p
        }
    }
}
