mod anon_const;
mod free_alias;
mod inherent;
mod opaque_types;

use rustc_type_ir::search_graph::IncreaseDepthForNested;
use rustc_type_ir::solve::QueryResultOrRerunNonErased;
use rustc_type_ir::{self as ty, Interner, ProjectionPredicate};
use tracing::{instrument, trace};

use crate::delegate::SolverDelegate;
use crate::solve::{
    Certainty, EvalCtxt, Goal, GoalEvaluation, GoalSource, NestedNormalizationGoals,
};

impl<D, I> EvalCtxt<'_, D>
where
    D: SolverDelegate<Interner = I>,
    I: Interner,
{
    #[instrument(level = "trace", skip(self), ret)]
    pub(super) fn compute_projection_goal(
        &mut self,
        goal: Goal<I, ProjectionPredicate<I>>,
    ) -> QueryResultOrRerunNonErased<I> {
        match goal.predicate.projection_term.kind {
            ty::AliasTermKind::ProjectionTy { .. } | ty::AliasTermKind::ProjectionConst { .. } => {
                self.normalize_associated_term(goal)
            }
            ty::AliasTermKind::InherentTy { .. } | ty::AliasTermKind::InherentConst { .. } => {
                self.normalize_inherent_associated_term(goal)
            }
            ty::AliasTermKind::OpaqueTy { .. } => self.normalize_opaque_type(goal),
            ty::AliasTermKind::FreeTy { .. } | ty::AliasTermKind::FreeConst { .. } => {
                self.normalize_free_alias(goal).map_err(Into::into)
            }
            ty::AliasTermKind::AnonConst { .. } => {
                self.normalize_anon_const(goal).map_err(Into::into)
            }
        }
    }

    fn normalize_associated_term(
        &mut self,
        goal: Goal<I, ProjectionPredicate<I>>,
    ) -> QueryResultOrRerunNonErased<I> {
        let ty::ProjectionPredicate { projection_term: alias, term } = goal.predicate;
        let unconstrained_term = self.next_term_infer_of_alias_kind(alias);
        let normalizes_to =
            goal.with(self.cx(), ty::NormalizesTo { alias, term: unconstrained_term });

        // We don't want candidate selection when normalizing associated terms to be impacted by
        // the expected term. Normalization should behave like a function of just the alias being
        // normalized. Because of this, we use an internal `NormalizesTo` goal for which the
        // expected term is always fully unconstrained and then equate that to the actual expected
        // term afterwards.
        //
        // But this results in weaker type inference if there's a nested where-clause when
        // normalizing which we can actually make progress on based on the expected term. To avoid
        // this, we return ambiguous nested goals for `NormalizesTo` to this context and register
        // them as if they were this `Projection` goal's own nested goals. (See #122687)
        //
        // After registering those goals, we equate the original expected term to the normalization
        // result. Although equating them can add inference constraint to the alias term, we don't
        // reevaluate this special `NormalizesTo` goal. But that's fine because as those inference
        // constraints should cause the caller to reevaluate the current `Projection `goal, which
        // will then also reevaluate the `NormalizesTo` goal.
        let (
            NestedNormalizationGoals(nested_goals),
            GoalEvaluation { goal: _, certainty, stalled_on: _, has_changed: _ },
        ) = self.evaluate_goal_raw(
            GoalSource::TypeRelating,
            normalizes_to,
            None,
            // We don't increase depth for nested goals for this `NormalizesTo` goal, as
            // evaluating `NormalizesTo` is an extra step only exists in the new solver
            // that behaves like a function call rather than an independent nested goal
            // evaluation, so increasing the depth may end up regressions which hit the
            // recursion limits for crates compiled well with the old solver. Furthermore,
            // those nested goals from `NormalizesTo` will be evaluated again as the
            // caller's nested goals with increased depths anyway.
            IncreaseDepthForNested::No,
        )?;

        trace!(?nested_goals);

        // Add a `make_canonical_response` probe step so that we treat this as
        // a candidate, even if `try_evaluate_added_goals` bails due to an error.
        // It's `Certainty::AMBIGUOUS` because this candidate is not "finished",
        // since equating the normalized terms will lead to additional constraints.
        self.inspect.make_canonical_response(Certainty::AMBIGUOUS);

        self.eq(goal.param_env, term, unconstrained_term)?;

        // Add the nested goals from normalization to our own nested goals.
        for (s, g) in nested_goals {
            self.add_goal(s, g)?;
        }

        self.evaluate_added_goals_and_make_canonical_response(certainty)
    }
}
