//! An infrastructure to mechanically analyse proof trees.
//!
//! It is unavoidable that this representation is somewhat
//! lossy as it should hide quite a few semantically relevant things,
//! e.g. canonicalization and the order of nested goals.
//!
//! @lcnr: However, a lot of the weirdness here is not strictly necessary
//! and could be improved in the future. This is mostly good enough for
//! coherence right now and was annoying to implement, so I am leaving it
//! as is until we start using it for something else.

use rustc_ast_ir::try_visit;
use rustc_ast_ir::visit::VisitorResult;
use rustc_infer::infer::resolve::EagerResolver;
use rustc_infer::infer::type_variable::TypeVariableOrigin;
use rustc_infer::infer::{DefineOpaqueTypes, InferCtxt, InferOk};
use rustc_middle::infer::unify_key::ConstVariableOrigin;
use rustc_middle::traits::query::NoSolution;
use rustc_middle::traits::solve::{inspect, QueryResult};
use rustc_middle::traits::solve::{Certainty, Goal};
use rustc_middle::traits::ObligationCause;
use rustc_middle::ty;
use rustc_middle::ty::TypeFoldable;
use rustc_span::Span;

use crate::solve::eval_ctxt::canonical;
use crate::solve::{EvalCtxt, GoalEvaluationKind, GoalSource};
use crate::solve::{GenerateProofTree, InferCtxtEvalExt};

pub struct InspectConfig {
    pub max_depth: usize,
}

pub struct InspectGoal<'a, 'tcx> {
    infcx: &'a InferCtxt<'tcx>,
    depth: usize,
    orig_values: &'a [ty::GenericArg<'tcx>],
    goal: Goal<'tcx, ty::Predicate<'tcx>>,
    evaluation: &'a inspect::GoalEvaluation<'tcx>,
}

pub struct InspectCandidate<'a, 'tcx> {
    goal: &'a InspectGoal<'a, 'tcx>,
    kind: inspect::ProbeKind<'tcx>,
    nested_goals: Vec<inspect::CanonicalState<'tcx, Goal<'tcx, ty::Predicate<'tcx>>>>,
    final_state: inspect::CanonicalState<'tcx, ()>,
    result: QueryResult<'tcx>,
}

impl<'a, 'tcx> InspectCandidate<'a, 'tcx> {
    pub fn kind(&self) -> inspect::ProbeKind<'tcx> {
        self.kind
    }

    pub fn result(&self) -> Result<Certainty, NoSolution> {
        self.result.map(|c| c.value.certainty)
    }

    /// Visit all nested goals of this candidate without rolling
    /// back their inference constraints. This function modifies
    /// the state of the `infcx`.
    pub fn visit_nested_no_probe<V: ProofTreeVisitor<'tcx>>(&self, visitor: &mut V) -> V::Result {
        if self.goal.depth < visitor.config().max_depth {
            let infcx = self.goal.infcx;
            let param_env = self.goal.goal.param_env;
            let mut orig_values = self.goal.orig_values.to_vec();
            let mut instantiated_goals = vec![];
            for goal in &self.nested_goals {
                let goal = canonical::instantiate_canonical_state(
                    infcx,
                    visitor.span(),
                    param_env,
                    &mut orig_values,
                    *goal,
                );
                instantiated_goals.push(goal);
            }

            let () = canonical::instantiate_canonical_state(
                infcx,
                visitor.span(),
                param_env,
                &mut orig_values,
                self.final_state,
            );

            for &goal in &instantiated_goals {
                let proof_tree = match goal.predicate.kind().no_bound_vars() {
                    Some(ty::PredicateKind::NormalizesTo(ty::NormalizesTo { alias, term })) => {
                        let unconstrained_term = match term.unpack() {
                            ty::TermKind::Ty(_) => infcx
                                .next_ty_var(TypeVariableOrigin {
                                    param_def_id: None,
                                    span: visitor.span(),
                                })
                                .into(),
                            ty::TermKind::Const(ct) => infcx
                                .next_const_var(
                                    ct.ty(),
                                    ConstVariableOrigin {
                                        param_def_id: None,
                                        span: visitor.span(),
                                    },
                                )
                                .into(),
                        };
                        let goal = goal
                            .with(infcx.tcx, ty::NormalizesTo { alias, term: unconstrained_term });
                        let proof_tree =
                            EvalCtxt::enter_root(infcx, GenerateProofTree::Yes, |ecx| {
                                ecx.evaluate_goal_raw(
                                    GoalEvaluationKind::Root,
                                    GoalSource::Misc,
                                    goal,
                                )
                            })
                            .1;
                        let InferOk { value: (), obligations: _ } = infcx
                            .at(&ObligationCause::dummy(), param_env)
                            .eq(DefineOpaqueTypes::Yes, term, unconstrained_term)
                            .unwrap();
                        proof_tree
                    }
                    _ => infcx.evaluate_root_goal(goal, GenerateProofTree::Yes).1,
                };
                try_visit!(visitor.visit_goal(&InspectGoal::new(
                    infcx,
                    self.goal.depth + 1,
                    &proof_tree.unwrap(),
                )));
            }
        }

        V::Result::output()
    }

    /// Visit all nested goals of this candidate, rolling back
    /// all inference constraints.
    pub fn visit_nested_in_probe<V: ProofTreeVisitor<'tcx>>(&self, visitor: &mut V) -> V::Result {
        self.goal.infcx.probe(|_| self.visit_nested_no_probe(visitor))
    }
}

impl<'a, 'tcx> InspectGoal<'a, 'tcx> {
    pub fn infcx(&self) -> &'a InferCtxt<'tcx> {
        self.infcx
    }

    pub fn goal(&self) -> Goal<'tcx, ty::Predicate<'tcx>> {
        self.goal
    }

    pub fn result(&self) -> Result<Certainty, NoSolution> {
        self.evaluation.evaluation.result.map(|c| c.value.certainty)
    }

    fn candidates_recur(
        &'a self,
        candidates: &mut Vec<InspectCandidate<'a, 'tcx>>,
        nested_goals: &mut Vec<inspect::CanonicalState<'tcx, Goal<'tcx, ty::Predicate<'tcx>>>>,
        probe: &inspect::Probe<'tcx>,
    ) {
        let num_candidates = candidates.len();
        for step in &probe.steps {
            match step {
                &inspect::ProbeStep::AddGoal(_source, goal) => nested_goals.push(goal),
                inspect::ProbeStep::NestedProbe(ref probe) => {
                    // Nested probes have to prove goals added in their parent
                    // but do not leak them, so we truncate the added goals
                    // afterwards.
                    let num_goals = nested_goals.len();
                    self.candidates_recur(candidates, nested_goals, probe);
                    nested_goals.truncate(num_goals);
                }
                inspect::ProbeStep::EvaluateGoals(_) => (),
            }
        }

        match probe.kind {
            inspect::ProbeKind::NormalizedSelfTyAssembly
            | inspect::ProbeKind::UnsizeAssembly
            | inspect::ProbeKind::UpcastProjectionCompatibility => (),
            // We add a candidate for the root evaluation if there
            // is only one way to prove a given goal, e.g. for `WellFormed`.
            //
            // FIXME: This is currently wrong if we don't even try any
            // candidates, e.g. for a trait goal, as in this case `candidates` is
            // actually supposed to be empty.
            inspect::ProbeKind::Root { result }
            | inspect::ProbeKind::TryNormalizeNonRigid { result } => {
                if candidates.len() == num_candidates {
                    candidates.push(InspectCandidate {
                        goal: self,
                        kind: probe.kind,
                        nested_goals: nested_goals.clone(),
                        final_state: probe.final_state,
                        result,
                    })
                }
            }
            inspect::ProbeKind::MiscCandidate { name: _, result }
            | inspect::ProbeKind::TraitCandidate { source: _, result } => {
                candidates.push(InspectCandidate {
                    goal: self,
                    kind: probe.kind,
                    nested_goals: nested_goals.clone(),
                    final_state: probe.final_state,
                    result,
                });
            }
        }
    }

    pub fn candidates(&'a self) -> Vec<InspectCandidate<'a, 'tcx>> {
        let mut candidates = vec![];
        let last_eval_step = match self.evaluation.evaluation.kind {
            inspect::CanonicalGoalEvaluationKind::Overflow
            | inspect::CanonicalGoalEvaluationKind::CycleInStack
            | inspect::CanonicalGoalEvaluationKind::ProvisionalCacheHit => {
                warn!("unexpected root evaluation: {:?}", self.evaluation);
                return vec![];
            }
            inspect::CanonicalGoalEvaluationKind::Evaluation { revisions } => {
                if let Some(last) = revisions.last() {
                    last
                } else {
                    return vec![];
                }
            }
        };

        let mut nested_goals = vec![];
        self.candidates_recur(&mut candidates, &mut nested_goals, &last_eval_step.evaluation);

        candidates
    }

    /// Returns the single candidate applicable for the current goal, if it exists.
    ///
    /// Returns `None` if there are either no or multiple applicable candidates.
    pub fn unique_applicable_candidate(&'a self) -> Option<InspectCandidate<'a, 'tcx>> {
        // FIXME(-Znext-solver): This does not handle impl candidates
        // hidden by env candidates.
        let mut candidates = self.candidates();
        candidates.retain(|c| c.result().is_ok());
        candidates.pop().filter(|_| candidates.is_empty())
    }

    fn new(
        infcx: &'a InferCtxt<'tcx>,
        depth: usize,
        root: &'a inspect::GoalEvaluation<'tcx>,
    ) -> Self {
        match root.kind {
            inspect::GoalEvaluationKind::Root { ref orig_values } => InspectGoal {
                infcx,
                depth,
                orig_values,
                goal: root.uncanonicalized_goal.fold_with(&mut EagerResolver::new(infcx)),
                evaluation: root,
            },
            inspect::GoalEvaluationKind::Nested { .. } => unreachable!(),
        }
    }
}

/// The public API to interact with proof trees.
pub trait ProofTreeVisitor<'tcx> {
    type Result: VisitorResult = ();

    fn span(&self) -> Span;

    fn config(&self) -> InspectConfig {
        InspectConfig { max_depth: 10 }
    }

    fn visit_goal(&mut self, goal: &InspectGoal<'_, 'tcx>) -> Self::Result;
}

#[extension(pub trait ProofTreeInferCtxtExt<'tcx>)]
impl<'tcx> InferCtxt<'tcx> {
    fn visit_proof_tree<V: ProofTreeVisitor<'tcx>>(
        &self,
        goal: Goal<'tcx, ty::Predicate<'tcx>>,
        visitor: &mut V,
    ) -> V::Result {
        let (_, proof_tree) = self.evaluate_root_goal(goal, GenerateProofTree::Yes);
        let proof_tree = proof_tree.unwrap();
        visitor.visit_goal(&InspectGoal::new(self, 0, &proof_tree))
    }
}
