use core::fmt;
use std::hash::{Hash, Hasher};

use derive_where::derive_where;
#[cfg(feature = "nightly")]
use rustc_macros::{Decodable_NoContext, Encodable_NoContext, StableHash_NoContext};

use crate::fold::TypeFoldable;
use crate::inherent::*;
use crate::relate::RelateResult;
use crate::relate::combine::PredicateEmittingRelation;
use crate::solve::VisibleForLeakCheck;
use crate::{self as ty, Interner, TyVid};

mod private {
    pub trait Sealed {}

    impl Sealed for super::CantBeErased {}
    impl Sealed for super::MayBeErased {}
}
pub trait TypingModeErasedStatus: private::Sealed + Clone + Copy + Hash + fmt::Debug {}

#[derive(Clone, Copy, Hash, Debug)]
pub enum CantBeErased {}
#[derive(Clone, Copy, Hash, Debug)]
#[cfg_attr(
    feature = "nightly",
    derive(Encodable_NoContext, Decodable_NoContext, StableHash_NoContext)
)]
pub struct MayBeErased;

impl TypingModeErasedStatus for CantBeErased {}
impl TypingModeErasedStatus for MayBeErased {}

/// The current typing mode of an inference context. We unfortunately have some
/// slightly different typing rules depending on the current context. See the
/// doc comment for each variant for how and why they are used.
///
/// In most cases you can get the correct typing mode automatically via:
/// - `mir::Body::typing_mode`
/// - `rustc_lint::LateContext::typing_mode`
///
/// If neither of these functions are available, feel free to reach out to
/// t-types for help.
///
/// Because typing rules get subtly different based on what typing mode we're in,
/// subtle enough that changing the behavior of typing modes can sometimes cause
/// changes that we don't even have tests for, we'd like to enforce the rule that
/// any place where we specialize behavior based on the typing mode, we match
/// *exhaustively* on the typing mode. That way, it's easy to determine all the
/// places that must change when anything about typing modes changes.
///
/// Hence, `TypingMode` does not implement `Eq`, though [`TypingModeEqWrapper`] is available
/// in the rare case that you do need this. Most cases where this currently matters is
/// where we pass typing modes through the query system and want to cache based on it.
/// See also `#[rustc_must_match_exhaustively]`, which tries to detect non-exhaustive
/// matches.
///
/// Since matching on typing mode to single out `Coherence` is so common, and `Coherence`
/// is so different from the other modes: see also [`is_coherence`](TypingMode::is_coherence)
#[derive_where(Clone, Copy, Hash, Debug; I: Interner)]
#[cfg_attr(
    feature = "nightly",
    derive(Encodable_NoContext, Decodable_NoContext, StableHash_NoContext)
)]
pub enum TypingMode<I: Interner, S: TypingModeErasedStatus = MayBeErased> {
    /// When checking whether impls overlap, we check whether any obligations
    /// are guaranteed to never hold when unifying the impls. This requires us
    /// to be complete: we must never fail to prove something which may actually
    /// hold.
    ///
    /// In this typing mode we bail with ambiguity in case its not knowable
    /// whether a trait goal may hold, e.g. because the trait may get implemented
    /// in a downstream or sibling crate.
    ///
    /// We also have to be careful when generalizing aliases inside of higher-ranked
    /// types to not unnecessarily constrain any inference variables.
    Coherence,
    /// Analysis includes type inference, checking that items are well-formed, and
    /// pretty much everything else which may emit proper type errors to the user.
    ///
    /// We only normalize opaque types which may get defined by the current body,
    /// which are stored in `defining_opaque_types`.
    ///
    /// We also refuse to project any associated type that is marked `default`.
    /// Non-`default` ("final") types are always projected. This is necessary in
    /// general for soundness of specialization. However, we *could* allow projections
    /// in fully-monomorphic cases. We choose not to, because we prefer for `default type`
    /// to force the type definition to be treated abstractly by any consumers of the
    /// impl. Concretely, that means that the following example will
    /// fail to compile:
    ///
    /// ```compile_fail,E0308
    /// #![feature(specialization)]
    /// trait Assoc {
    ///     type Output;
    /// }
    ///
    /// impl<T> Assoc for T {
    ///     default type Output = bool;
    /// }
    ///
    /// fn main() {
    ///     let x: <() as Assoc>::Output = true;
    /// }
    /// ```
    Analysis { defining_opaque_types_and_generators: I::LocalDefIds },
    /// The behavior during MIR borrowck is identical to `TypingMode::Analysis`
    /// except that the initial value for opaque types is the type computed during
    /// HIR typeck with unique unconstrained region inference variables.
    ///
    /// This is currently only used with by the new solver as it results in new
    /// non-universal defining uses of opaque types, which is a breaking change.
    /// See tests/ui/impl-trait/non-defining-use/as-projection-term.rs.
    Borrowck { defining_opaque_types: I::LocalDefIds },
    /// Any analysis after borrowck for a given body should be able to use all the
    /// hidden types defined by borrowck, without being able to define any new ones.
    ///
    /// This is currently only used by the new solver, but should be implemented in
    /// the old solver as well.
    PostBorrowckAnalysis { defined_opaque_types: I::LocalDefIds },
    /// After analysis, mostly during codegen and MIR optimizations, we're able to
    /// reveal all opaque types. As the hidden type should *never* be observable
    /// directly by the user, this should not be used by checks which may expose
    /// such details to the user.
    ///
    /// There are some exceptions to this as for example `layout_of` and const-evaluation
    /// always run in `PostAnalysis` mode, even when used during analysis. This exposes
    /// some information about the underlying type to users, but not the type itself.
    PostAnalysis,

    /// The typing modes above (except coherence) only differ in how they handle
    ///
    /// - Generators
    /// - Opaque types
    /// - Specialization (in `PostAnalysis`)
    ///
    /// We replace all of them with this `TypingMode` in the first attempt at canonicalization.
    /// If, during that attempt, we try to access information about opaques or generators
    /// we bail out, setting a field on `EvalCtxt` that indicates the canonicalization must be
    /// rerun in the original typing mode.
    ///
    /// `TypingMode::Coherence` is not replaced by this and is always kept as-is.
    ErasedNotCoherence(S),
}

/// We want to highly discourage using equality checks on typing modes.
/// Instead you should match, **exhaustively**, so when we ever modify the enum we get a compile
/// error. Only use `TypingModeEqWrapper` when you really really really have to.
/// Prefer unwrapping `TypingModeEqWrapper` in apis that should return a `TypingMode` whenever
/// possible, and if you ever get an `TypingModeEqWrapper`, prefer unwrapping it and matching on it **exhaustively**.
#[derive_where(Clone, Copy, Debug; I: Interner)]
#[cfg_attr(
    feature = "nightly",
    derive(Encodable_NoContext, Decodable_NoContext, StableHash_NoContext)
)]
pub struct TypingModeEqWrapper<I: Interner>(pub TypingMode<I, MayBeErased>);

impl<I: Interner> Hash for TypingModeEqWrapper<I> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.hash(state);
    }
}

impl<I: Interner> PartialEq for TypingModeEqWrapper<I> {
    fn eq(&self, other: &Self) -> bool {
        match (self.0, other.0) {
            (TypingMode::Coherence, TypingMode::Coherence) => true,
            (
                TypingMode::Analysis { defining_opaque_types_and_generators: l },
                TypingMode::Analysis { defining_opaque_types_and_generators: r },
            ) => l == r,
            (
                TypingMode::Borrowck { defining_opaque_types: l },
                TypingMode::Borrowck { defining_opaque_types: r },
            ) => l == r,
            (
                TypingMode::PostBorrowckAnalysis { defined_opaque_types: l },
                TypingMode::PostBorrowckAnalysis { defined_opaque_types: r },
            ) => l == r,
            (TypingMode::PostAnalysis, TypingMode::PostAnalysis) => true,
            (
                TypingMode::ErasedNotCoherence(MayBeErased),
                TypingMode::ErasedNotCoherence(MayBeErased),
            ) => true,
            (
                TypingMode::Coherence
                | TypingMode::Analysis { .. }
                | TypingMode::Borrowck { .. }
                | TypingMode::PostBorrowckAnalysis { .. }
                | TypingMode::PostAnalysis
                | TypingMode::ErasedNotCoherence(MayBeErased),
                _,
            ) => false,
        }
    }
}

impl<I: Interner> Eq for TypingModeEqWrapper<I> {}

impl<I: Interner, S: TypingModeErasedStatus> TypingMode<I, S> {
    /// There are a bunch of places in the compiler where we single out `Coherence`,
    /// and alter behavior. We'd like to *always* match on `TypingMode` exhaustively,
    /// but not having this method leads to a bunch of noisy code.
    ///
    /// See also the documentation on [`TypingMode`] about exhaustive matching.
    pub fn is_coherence(&self) -> bool {
        match self {
            TypingMode::Coherence => true,
            TypingMode::Analysis { .. }
            | TypingMode::Borrowck { .. }
            | TypingMode::PostBorrowckAnalysis { .. }
            | TypingMode::PostAnalysis
            | TypingMode::ErasedNotCoherence(_) => false,
        }
    }

    /// There are a bunch of places in the trait solver where we single out `Coherence`,
    /// and alter behavior. We'd like to *always* match on `TypingMode` exhaustively,
    /// but not having this method leads to a bunch of noisy code.
    ///
    /// See also the documentation on [`TypingMode`] about exhaustive matching.
    pub fn is_erased_not_coherence(&self) -> bool {
        match self {
            TypingMode::ErasedNotCoherence(_) => true,
            TypingMode::Coherence
            | TypingMode::Analysis { .. }
            | TypingMode::Borrowck { .. }
            | TypingMode::PostBorrowckAnalysis { .. }
            | TypingMode::PostAnalysis => false,
        }
    }
}

impl<I: Interner> TypingMode<I, MayBeErased> {
    /// Only call this when you're sure you're outside the next trait solver!
    /// That means either not in the trait solver, or in code that is old-solver only.
    ///
    /// See the comment on `InferCtxt::typing_mode_raw`
    pub fn assert_not_erased(self) -> TypingMode<I, CantBeErased> {
        match self {
            TypingMode::Coherence => TypingMode::Coherence,
            TypingMode::Analysis { defining_opaque_types_and_generators } => {
                TypingMode::Analysis { defining_opaque_types_and_generators }
            }
            TypingMode::Borrowck { defining_opaque_types } => {
                TypingMode::Borrowck { defining_opaque_types }
            }
            TypingMode::PostBorrowckAnalysis { defined_opaque_types } => {
                TypingMode::PostBorrowckAnalysis { defined_opaque_types }
            }
            TypingMode::PostAnalysis => TypingMode::PostAnalysis,
            TypingMode::ErasedNotCoherence(MayBeErased) => panic!(
                "Called `assert_not_erased` from a place that can be called by the trait solver in `TypingMode::ErasedNotCoherence`. `TypingMode` is `ErasedNotCoherence` in a place where that should be impossible"
            ),
        }
    }
}

impl<I: Interner> TypingMode<I, CantBeErased> {
    /// Analysis outside of a body does not define any opaque types.
    pub fn non_body_analysis() -> TypingMode<I> {
        TypingMode::Analysis { defining_opaque_types_and_generators: Default::default() }
    }

    pub fn typeck_for_body(cx: I, body_def_id: I::LocalDefId) -> TypingMode<I> {
        TypingMode::Analysis {
            defining_opaque_types_and_generators: cx
                .opaque_types_and_coroutines_defined_by(body_def_id),
        }
    }

    /// While typechecking a body, we need to be able to define the opaque
    /// types defined by that body.
    ///
    /// FIXME: This will be removed because it's generally not correct to define
    /// opaques outside of HIR typeck.
    pub fn analysis_in_body(cx: I, body_def_id: I::LocalDefId) -> TypingMode<I> {
        TypingMode::Analysis {
            defining_opaque_types_and_generators: cx.opaque_types_defined_by(body_def_id),
        }
    }

    pub fn borrowck(cx: I, body_def_id: I::LocalDefId) -> TypingMode<I> {
        let defining_opaque_types = cx.opaque_types_defined_by(body_def_id);
        if defining_opaque_types.is_empty() {
            TypingMode::non_body_analysis()
        } else {
            TypingMode::Borrowck { defining_opaque_types }
        }
    }

    pub fn post_borrowck_analysis(cx: I, body_def_id: I::LocalDefId) -> TypingMode<I> {
        let defined_opaque_types = cx.opaque_types_defined_by(body_def_id);
        if defined_opaque_types.is_empty() {
            TypingMode::non_body_analysis()
        } else {
            TypingMode::PostBorrowckAnalysis { defined_opaque_types }
        }
    }
}

impl<I: Interner> From<TypingMode<I, CantBeErased>> for TypingMode<I, MayBeErased> {
    fn from(value: TypingMode<I, CantBeErased>) -> Self {
        match value {
            TypingMode::Coherence => TypingMode::Coherence,
            TypingMode::Analysis { defining_opaque_types_and_generators } => {
                TypingMode::Analysis { defining_opaque_types_and_generators }
            }
            TypingMode::Borrowck { defining_opaque_types } => {
                TypingMode::Borrowck { defining_opaque_types }
            }
            TypingMode::PostBorrowckAnalysis { defined_opaque_types } => {
                TypingMode::PostBorrowckAnalysis { defined_opaque_types }
            }
            TypingMode::PostAnalysis => TypingMode::PostAnalysis,
        }
    }
}

#[cfg_attr(feature = "nightly", rustc_diagnostic_item = "type_ir_infer_ctxt_like")]
pub trait InferCtxtLike: Sized {
    type Interner: Interner;
    fn cx(&self) -> Self::Interner;

    /// Whether the new trait solver is enabled. This only exists because rustc
    /// shares code between the new and old trait solvers; for all other users,
    /// this should always be true. If this is unknowingly false and you try to
    /// use the new trait solver, things will break badly.
    fn next_trait_solver(&self) -> bool {
        true
    }

    fn disable_trait_solver_fast_paths(&self) -> bool;

    fn typing_mode_raw(&self) -> TypingMode<Self::Interner>;

    fn universe(&self) -> ty::UniverseIndex;
    fn create_next_universe(&self) -> ty::UniverseIndex;

    fn insert_placeholder_assumptions(
        &self,
        u: ty::UniverseIndex,
        assumptions: Option<crate::region_constraint::Assumptions<Self::Interner>>,
    );
    fn get_placeholder_assumptions(
        &self,
        u: ty::UniverseIndex,
    ) -> Option<crate::region_constraint::Assumptions<Self::Interner>>;
    fn get_solver_region_constraint(
        &self,
    ) -> crate::region_constraint::RegionConstraint<Self::Interner>;
    fn overwrite_solver_region_constraint(
        &self,
        constraint: crate::region_constraint::RegionConstraint<Self::Interner>,
    );

    fn universe_of_ty(&self, ty: ty::TyVid) -> Option<ty::UniverseIndex>;
    fn universe_of_lt(&self, lt: ty::RegionVid) -> Option<ty::UniverseIndex>;
    fn universe_of_ct(&self, ct: ty::ConstVid) -> Option<ty::UniverseIndex>;

    fn root_ty_var(&self, var: ty::TyVid) -> ty::TyVid;
    fn sub_unification_table_root_var(&self, var: ty::TyVid) -> ty::TyVid;
    fn root_const_var(&self, var: ty::ConstVid) -> ty::ConstVid;

    fn opportunistic_resolve_ty_var(&self, vid: ty::TyVid) -> <Self::Interner as Interner>::Ty;
    fn opportunistic_resolve_int_var(&self, vid: ty::IntVid) -> <Self::Interner as Interner>::Ty;
    fn opportunistic_resolve_float_var(
        &self,
        vid: ty::FloatVid,
    ) -> <Self::Interner as Interner>::Ty;
    fn opportunistic_resolve_ct_var(
        &self,
        vid: ty::ConstVid,
    ) -> <Self::Interner as Interner>::Const;
    fn opportunistic_resolve_lt_var(
        &self,
        vid: ty::RegionVid,
    ) -> <Self::Interner as Interner>::Region;

    fn is_changed_arg(&self, arg: <Self::Interner as Interner>::GenericArg) -> bool;

    fn next_region_infer(&self) -> <Self::Interner as Interner>::Region;
    fn next_ty_infer(&self) -> <Self::Interner as Interner>::Ty;
    fn next_const_infer(&self) -> <Self::Interner as Interner>::Const;
    fn fresh_args_for_item(
        &self,
        def_id: <Self::Interner as Interner>::DefId,
    ) -> <Self::Interner as Interner>::GenericArgs;

    fn instantiate_binder_with_infer<T: TypeFoldable<Self::Interner> + Copy>(
        &self,
        value: ty::Binder<Self::Interner, T>,
    ) -> T;

    fn enter_forall<T: TypeFoldable<Self::Interner>, U>(
        &self,
        value: ty::Binder<Self::Interner, T>,
        f: impl FnOnce(T) -> U,
    ) -> U;

    fn equate_ty_vids_raw(&self, a: ty::TyVid, b: ty::TyVid);
    fn sub_unify_ty_vids_raw(&self, a: ty::TyVid, b: ty::TyVid);
    fn equate_int_vids_raw(&self, a: ty::IntVid, b: ty::IntVid);
    fn equate_float_vids_raw(&self, a: ty::FloatVid, b: ty::FloatVid);
    fn equate_const_vids_raw(&self, a: ty::ConstVid, b: ty::ConstVid);

    fn instantiate_ty_var_raw<R: PredicateEmittingRelation<Self>>(
        &self,
        relation: &mut R,
        target_is_expected: bool,
        target_vid: ty::TyVid,
        instantiation_variance: ty::Variance,
        source_ty: <Self::Interner as Interner>::Ty,
    ) -> RelateResult<Self::Interner, ()>;
    fn instantiate_int_var_raw(&self, vid: ty::IntVid, value: ty::IntVarValue);
    fn instantiate_float_var_raw(&self, vid: ty::FloatVid, value: ty::FloatVarValue);
    fn instantiate_const_var_raw<R: PredicateEmittingRelation<Self>>(
        &self,
        relation: &mut R,
        target_is_expected: bool,
        target_vid: ty::ConstVid,
        source_ct: <Self::Interner as Interner>::Const,
    ) -> RelateResult<Self::Interner, ()>;

    fn set_tainted_by_errors(&self, e: <Self::Interner as Interner>::ErrorGuaranteed);

    fn shallow_resolve(
        &self,
        ty: <Self::Interner as Interner>::Ty,
    ) -> <Self::Interner as Interner>::Ty;
    fn shallow_resolve_const(
        &self,
        ty: <Self::Interner as Interner>::Const,
    ) -> <Self::Interner as Interner>::Const;

    fn resolve_vars_if_possible<T>(&self, value: T) -> T
    where
        T: TypeFoldable<Self::Interner>;

    fn probe<T>(&self, probe: impl FnOnce() -> T) -> T;

    fn sub_regions(
        &self,
        sub: <Self::Interner as Interner>::Region,
        sup: <Self::Interner as Interner>::Region,
        vis: VisibleForLeakCheck,
        span: <Self::Interner as Interner>::Span,
    );

    fn equate_regions(
        &self,
        a: <Self::Interner as Interner>::Region,
        b: <Self::Interner as Interner>::Region,
        vis: VisibleForLeakCheck,
        span: <Self::Interner as Interner>::Span,
    );

    fn register_solver_region_constraint(
        &self,
        c: crate::region_constraint::RegionConstraint<Self::Interner>,
    );

    fn register_ty_outlives(
        &self,
        ty: <Self::Interner as Interner>::Ty,
        r: <Self::Interner as Interner>::Region,
        span: <Self::Interner as Interner>::Span,
    );

    type OpaqueTypeStorageEntries: OpaqueTypeStorageEntries;
    fn opaque_types_storage_num_entries(&self) -> Self::OpaqueTypeStorageEntries;
    fn clone_opaque_types_lookup_table(
        &self,
    ) -> Vec<(ty::OpaqueTypeKey<Self::Interner>, <Self::Interner as Interner>::Ty)>;
    fn clone_duplicate_opaque_types(
        &self,
    ) -> Vec<(ty::OpaqueTypeKey<Self::Interner>, <Self::Interner as Interner>::Ty)>;
    fn clone_opaque_types_added_since(
        &self,
        prev_entries: Self::OpaqueTypeStorageEntries,
    ) -> Vec<(ty::OpaqueTypeKey<Self::Interner>, <Self::Interner as Interner>::Ty)>;
    fn opaques_with_sub_unified_hidden_type(&self, ty: TyVid) -> Vec<ty::AliasTy<Self::Interner>>;

    fn register_hidden_type_in_storage(
        &self,
        opaque_type_key: ty::OpaqueTypeKey<Self::Interner>,
        hidden_ty: <Self::Interner as Interner>::Ty,
        span: <Self::Interner as Interner>::Span,
    ) -> Option<<Self::Interner as Interner>::Ty>;
    fn add_duplicate_opaque_type(
        &self,
        opaque_type_key: ty::OpaqueTypeKey<Self::Interner>,
        hidden_ty: <Self::Interner as Interner>::Ty,
        span: <Self::Interner as Interner>::Span,
    );

    fn reset_opaque_types(&self);
}

pub fn may_use_unstable_feature<'a, I: Interner, Infcx>(
    infcx: &'a Infcx,
    param_env: I::ParamEnv,
    symbol: I::Symbol,
) -> bool
where
    Infcx: InferCtxtLike<Interner = I>,
{
    // Iterate through all goals in param_env to find the one that has the same symbol.
    for pred in param_env.caller_bounds().iter() {
        if let ty::ClauseKind::UnstableFeature(sym) = pred.kind().skip_binder() {
            if sym == symbol {
                return true;
            }
        }
    }

    // During codegen we must assume that all feature bounds hold as we may be
    // monomorphizing a body from an upstream crate which had an unstable feature
    // enabled that we do not.
    //
    // Coherence should already report overlap errors involving unstable impls
    // as the affected code would otherwise break when stabilizing this feature.
    // It is also easily possible to accidentally cause unsoundness this way as
    // we have to always enable unstable impls during codegen.
    //
    // Return ambiguity can also prevent people from writing code which depends on inference guidance
    // that might no longer work after the impl is stabilised,
    // tests/ui/unstable-feature-bound/unstable_impl_method_selection.rs is one of the example.
    //
    // Note: `feature_bound_holds_in_crate` does not consider a feature to be enabled
    // if we are in std/core even if there is a corresponding `feature` attribute on the crate.

    match infcx.typing_mode_raw().assert_not_erased() {
        TypingMode::Coherence
        | TypingMode::Analysis { .. }
        | TypingMode::Borrowck { .. }
        | TypingMode::PostBorrowckAnalysis { .. } => {
            infcx.cx().features().feature_bound_holds_in_crate(symbol)
        }
        TypingMode::PostAnalysis => true,
    }
}
