//! Code for the 'normalization' query. This consists of a wrapper
//! which folds deeply, invoking the underlying
//! `normalize_canonicalized_projection` query when it encounters projections.

use rustc_data_structures::sso::SsoHashMap;
use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_infer::traits::PredicateObligations;
use rustc_macros::extension;
pub use rustc_middle::traits::query::NormalizationResult;
use rustc_middle::ty::{
    self, FallibleTypeFolder, Flags, Ty, TyCtxt, TypeFoldable, TypeSuperFoldable,
    TypeSuperVisitable, TypeVisitable, TypeVisitableExt, TypeVisitor, TypingMode, Unnormalized,
};
use rustc_span::DUMMY_SP;
use tracing::{debug, info, instrument};

use super::NoSolution;
use crate::error_reporting::InferCtxtErrorExt;
use crate::error_reporting::traits::OverflowCause;
use crate::infer::at::At;
use crate::infer::canonical::OriginalQueryValues;
use crate::infer::{InferCtxt, InferOk};
use crate::traits::normalize::needs_normalization;
use crate::traits::{
    BoundVarReplacer, Normalized, ObligationCause, PlaceholderReplacer, ScrubbedTraitError,
};

#[extension(pub trait QueryNormalizeExt<'tcx>)]
impl<'a, 'tcx> At<'a, 'tcx> {
    /// Normalize `value` in the context of the inference context,
    /// yielding a resulting type, or an error if `value` cannot be
    /// normalized. If you don't care about regions, you should prefer
    /// `normalize_erasing_regions`, which is more efficient.
    ///
    /// If the normalization succeeds, returns back the normalized
    /// value along with various outlives relations (in the form of
    /// obligations that must be discharged).
    ///
    /// This normalization should *only* be used when the projection is well-formed and
    /// does not have possible ambiguity (contains inference variables).
    ///
    /// After codegen, when lifetimes do not matter, it is preferable to instead
    /// use [`TyCtxt::normalize_erasing_regions`], which wraps this procedure.
    ///
    /// N.B. Once the new solver is stabilized this method of normalization will
    /// likely be removed as trait solver operations are already cached by the query
    /// system making this redundant.
    fn query_normalize<T>(self, value: T) -> Result<Normalized<'tcx, T>, NoSolution>
    where
        T: TypeFoldable<TyCtxt<'tcx>>,
    {
        debug!(
            "normalize::<{}>(value={:?}, param_env={:?}, cause={:?})",
            std::any::type_name::<T>(),
            value,
            self.param_env,
            self.cause,
        );

        // This is actually a consequence by the way `normalize_erasing_regions` works currently.
        // Because it needs to call the `normalize_generic_arg_after_erasing_regions`, it folds
        // through tys and consts in a `TypeFoldable`. Importantly, it skips binders, leaving us
        // with trying to normalize with escaping bound vars.
        //
        // Here, we just add the universes that we *would* have created had we passed through the binders.
        //
        // We *could* replace escaping bound vars eagerly here, but it doesn't seem really necessary.
        // The rest of the code is already set up to be lazy about replacing bound vars,
        // and only when we actually have to normalize.
        let universes = if value.has_escaping_bound_vars() {
            let mut max_visitor =
                MaxEscapingBoundVarVisitor { outer_index: ty::INNERMOST, escaping: 0 };
            value.visit_with(&mut max_visitor);
            vec![None; max_visitor.escaping]
        } else {
            vec![]
        };

        if self.infcx.next_trait_solver() {
            match crate::solve::deeply_normalize_with_skipped_universes::<_, ScrubbedTraitError<'tcx>>(
                self,
                Unnormalized::new_wip(value),
                universes,
            ) {
                Ok(value) => {
                    return Ok(Normalized { value, obligations: PredicateObligations::new() });
                }
                Err(_errors) => {
                    return Err(NoSolution);
                }
            }
        }

        if !needs_normalization(self.infcx, &value) {
            return Ok(Normalized { value, obligations: PredicateObligations::new() });
        }

        let mut normalizer = QueryNormalizer {
            infcx: self.infcx,
            cause: self.cause,
            param_env: self.param_env,
            obligations: PredicateObligations::new(),
            cache: SsoHashMap::new(),
            anon_depth: 0,
            universes,
        };

        let result = value.try_fold_with(&mut normalizer);
        info!(
            "normalize::<{}>: result={:?} with {} obligations",
            std::any::type_name::<T>(),
            result,
            normalizer.obligations.len(),
        );
        debug!(
            "normalize::<{}>: obligations={:?}",
            std::any::type_name::<T>(),
            normalizer.obligations,
        );
        result.map(|value| Normalized { value, obligations: normalizer.obligations })
    }
}

// Visitor to find the maximum escaping bound var
struct MaxEscapingBoundVarVisitor {
    // The index which would count as escaping
    outer_index: ty::DebruijnIndex,
    escaping: usize,
}

impl<'tcx> TypeVisitor<TyCtxt<'tcx>> for MaxEscapingBoundVarVisitor {
    fn visit_binder<T: TypeVisitable<TyCtxt<'tcx>>>(&mut self, t: &ty::Binder<'tcx, T>) {
        self.outer_index.shift_in(1);
        t.super_visit_with(self);
        self.outer_index.shift_out(1);
    }

    #[inline]
    fn visit_ty(&mut self, t: Ty<'tcx>) {
        if t.outer_exclusive_binder() > self.outer_index {
            self.escaping = self
                .escaping
                .max(t.outer_exclusive_binder().as_usize() - self.outer_index.as_usize());
        }
    }

    #[inline]
    fn visit_region(&mut self, r: ty::Region<'tcx>) {
        match r.kind() {
            ty::ReBound(ty::BoundVarIndexKind::Bound(debruijn), _)
                if debruijn > self.outer_index =>
            {
                self.escaping =
                    self.escaping.max(debruijn.as_usize() - self.outer_index.as_usize());
            }
            _ => {}
        }
    }

    fn visit_const(&mut self, ct: ty::Const<'tcx>) {
        if ct.outer_exclusive_binder() > self.outer_index {
            self.escaping = self
                .escaping
                .max(ct.outer_exclusive_binder().as_usize() - self.outer_index.as_usize());
        }
    }
}

struct QueryNormalizer<'a, 'tcx> {
    infcx: &'a InferCtxt<'tcx>,
    cause: &'a ObligationCause<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    obligations: PredicateObligations<'tcx>,
    cache: SsoHashMap<Ty<'tcx>, Ty<'tcx>>,
    anon_depth: usize,
    universes: Vec<Option<ty::UniverseIndex>>,
}

impl<'a, 'tcx> FallibleTypeFolder<TyCtxt<'tcx>> for QueryNormalizer<'a, 'tcx> {
    type Error = NoSolution;

    fn cx(&self) -> TyCtxt<'tcx> {
        self.infcx.tcx
    }

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

    #[instrument(level = "debug", skip(self))]
    fn try_fold_ty(&mut self, ty: Ty<'tcx>) -> Result<Ty<'tcx>, Self::Error> {
        if !needs_normalization(self.infcx, &ty) {
            return Ok(ty);
        }

        if let Some(ty) = self.cache.get(&ty) {
            return Ok(*ty);
        }

        let &ty::Alias(data) = ty.kind() else {
            let res = ty.try_super_fold_with(self)?;
            self.cache.insert(ty, res);
            return Ok(res);
        };

        // See note in `rustc_trait_selection::traits::project` about why we
        // wait to fold the args.
        let res = match data.kind {
            ty::Opaque { def_id } => {
                // Only normalize `impl Trait` outside of type inference, usually in codegen.
                match self.infcx.typing_mode_raw().assert_not_erased() {
                    TypingMode::Coherence
                    | TypingMode::Analysis { .. }
                    | TypingMode::Borrowck { .. }
                    | TypingMode::PostBorrowckAnalysis { .. } => ty.try_super_fold_with(self)?,

                    TypingMode::PostAnalysis | TypingMode::Codegen => {
                        let args = data.args.try_fold_with(self)?;
                        let recursion_limit = self.cx().recursion_limit();

                        if !recursion_limit.value_within_limit(self.anon_depth) {
                            let guar = self
                                .infcx
                                .err_ctxt()
                                .build_overflow_error(
                                    OverflowCause::DeeplyNormalize(data.into()),
                                    self.cause.span,
                                    true,
                                )
                                .delay_as_bug();
                            return Ok(Ty::new_error(self.cx(), guar));
                        }

                        let generic_ty = self.cx().type_of(def_id);
                        let mut concrete_ty =
                            generic_ty.instantiate(self.cx(), args).skip_norm_wip();
                        self.anon_depth += 1;
                        if concrete_ty == ty {
                            concrete_ty = Ty::new_error_with_message(
                                self.cx(),
                                DUMMY_SP,
                                "recursive opaque type",
                            );
                        }
                        let folded_ty = ensure_sufficient_stack(|| self.try_fold_ty(concrete_ty));
                        self.anon_depth -= 1;
                        folded_ty?
                    }
                }
            }

            kind @ (ty::Projection { .. } | ty::Inherent { .. } | ty::Free { .. }) => self
                .try_fold_free_or_assoc(ty::AliasTerm::new(self.cx(), kind.into(), data.args))?
                .expect_type(),
        };

        self.cache.insert(ty, res);
        Ok(res)
    }

    fn try_fold_const(
        &mut self,
        constant: ty::Const<'tcx>,
    ) -> Result<ty::Const<'tcx>, Self::Error> {
        if !needs_normalization(self.infcx, &constant) {
            return Ok(constant);
        }

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

        let constant = match uv.kind {
            ty::UnevaluatedConstKind::Anon { .. } => {
                crate::traits::with_replaced_escaping_bound_vars(
                    self.infcx,
                    &mut self.universes,
                    constant,
                    |constant| crate::traits::evaluate_const(&self.infcx, constant, self.param_env),
                )
            }
            _ => self.try_fold_free_or_assoc(uv.into())?.expect_const(),
        };
        debug!(?constant, ?self.param_env);
        constant.try_super_fold_with(self)
    }

    #[inline]
    fn try_fold_predicate(
        &mut self,
        p: ty::Predicate<'tcx>,
    ) -> Result<ty::Predicate<'tcx>, Self::Error> {
        if p.allow_normalization() && needs_normalization(self.infcx, &p) {
            p.try_super_fold_with(self)
        } else {
            Ok(p)
        }
    }
}

impl<'a, 'tcx> QueryNormalizer<'a, 'tcx> {
    fn try_fold_free_or_assoc(
        &mut self,
        term: ty::AliasTerm<'tcx>,
    ) -> Result<ty::Term<'tcx>, NoSolution> {
        let infcx = self.infcx;
        let tcx = infcx.tcx;
        // Just an optimization: When we don't have escaping bound vars,
        // we don't need to replace them with placeholders.
        let (term, maps) = if term.has_escaping_bound_vars() {
            let (term, mapped_regions, mapped_types, mapped_consts) =
                BoundVarReplacer::replace_bound_vars(infcx, &mut self.universes, term);
            (term, Some((mapped_regions, mapped_types, mapped_consts)))
        } else {
            (term, None)
        };
        let term = term.try_fold_with(self)?;

        let mut orig_values = OriginalQueryValues::default();
        let c_term = infcx.canonicalize_query(self.param_env.and(term), &mut orig_values);
        debug!("QueryNormalizer: c_term = {:#?}", c_term);
        debug!("QueryNormalizer: orig_values = {:#?}", orig_values);
        let result = match term.kind {
            ty::AliasTermKind::ProjectionTy { .. } | ty::AliasTermKind::ProjectionConst { .. } => {
                tcx.normalize_canonicalized_projection(c_term)
            }
            ty::AliasTermKind::FreeTy { .. } | ty::AliasTermKind::FreeConst { .. } => {
                tcx.normalize_canonicalized_free_alias(c_term)
            }
            ty::AliasTermKind::InherentTy { .. } | ty::AliasTermKind::InherentConst { .. } => {
                tcx.normalize_canonicalized_inherent_projection(c_term)
            }
            kind @ (ty::AliasTermKind::OpaqueTy { .. } | ty::AliasTermKind::AnonConst { .. }) => {
                unreachable!("did not expect {kind:?} due to match arm above")
            }
        }?;
        // We don't expect ambiguity.
        if !result.value.is_proven() {
            // Rustdoc normalizes possibly not well-formed types, so only
            // treat this as a bug if we're not in rustdoc.
            if !tcx.sess.opts.actually_rustdoc {
                tcx.dcx().delayed_bug(format!("unexpected ambiguity: {c_term:?} {result:?}"));
            }
            return Err(NoSolution);
        }
        let InferOk { value: result, obligations } = infcx
            .instantiate_query_response_and_region_obligations(
                self.cause,
                self.param_env,
                &orig_values,
                result,
            )?;
        debug!("QueryNormalizer: result = {:#?}", result);
        debug!("QueryNormalizer: obligations = {:#?}", obligations);
        self.obligations.extend(obligations);
        let res = if let Some((mapped_regions, mapped_types, mapped_consts)) = maps {
            PlaceholderReplacer::replace_placeholders(
                infcx,
                mapped_regions,
                mapped_types,
                mapped_consts,
                &self.universes,
                result.normalized_term,
            )
        } else {
            result.normalized_term
        };
        // `tcx.normalize_canonicalized_projection` may normalize to a type that
        // still has unevaluated consts, so keep normalizing here if that's the case.
        // Similarly, `tcx.normalize_canonicalized_free_alias` will only unwrap one layer
        // of type/const and we need to continue folding it to reveal the TAIT behind it
        // or further normalize nested unevaluated consts.
        if res != term.to_term(tcx)
            && (res.has_type_flags(ty::TypeFlags::HAS_CT_PROJECTION)
                || matches!(
                    term.kind,
                    ty::AliasTermKind::FreeTy { .. } | ty::AliasTermKind::FreeConst { .. }
                ))
        {
            res.try_fold_with(self)
        } else {
            Ok(res)
        }
    }
}
