//! Compute the dyn-compatibility of a trait

use std::ops::ControlFlow;

use hir_def::{
    AssocItemId, ConstId, CrateRootModuleId, FunctionId, GenericDefId, HasModule, TraitId,
    TypeAliasId, lang_item::LangItem, signatures::TraitFlags,
};
use intern::Symbol;
use rustc_hash::FxHashSet;
use rustc_type_ir::{
    AliasTyKind, ClauseKind, PredicatePolarity, TypeSuperVisitable as _, TypeVisitable as _,
    Upcast, elaborate,
    inherent::{IntoKind, SliceLike},
};
use smallvec::SmallVec;

use crate::{
    ImplTraitId,
    db::{HirDatabase, InternedOpaqueTyId},
    lower_nextsolver::associated_ty_item_bounds,
    next_solver::{
        Clause, Clauses, DbInterner, GenericArgs, ParamEnv, SolverDefId, TraitPredicate,
        TypingMode, infer::DbInternerInferExt, mk_param,
    },
    traits::next_trait_solve_in_ctxt,
};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DynCompatibilityViolation {
    SizedSelf,
    SelfReferential,
    Method(FunctionId, MethodViolationCode),
    AssocConst(ConstId),
    GAT(TypeAliasId),
    // This doesn't exist in rustc, but added for better visualization
    HasNonCompatibleSuperTrait(TraitId),
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MethodViolationCode {
    StaticMethod,
    ReferencesSelfInput,
    ReferencesSelfOutput,
    ReferencesImplTraitInTrait,
    AsyncFn,
    WhereClauseReferencesSelf,
    Generic,
    UndispatchableReceiver,
}

pub fn dyn_compatibility(
    db: &dyn HirDatabase,
    trait_: TraitId,
) -> Option<DynCompatibilityViolation> {
    let interner = DbInterner::new_with(db, Some(trait_.krate(db)), None);
    for super_trait in elaborate::supertrait_def_ids(interner, SolverDefId::TraitId(trait_)) {
        let super_trait = match super_trait {
            SolverDefId::TraitId(id) => id,
            _ => unreachable!(),
        };
        if let Some(v) = db.dyn_compatibility_of_trait(super_trait) {
            return if super_trait == trait_ {
                Some(v)
            } else {
                Some(DynCompatibilityViolation::HasNonCompatibleSuperTrait(super_trait))
            };
        }
    }

    None
}

pub fn dyn_compatibility_with_callback<F>(
    db: &dyn HirDatabase,
    trait_: TraitId,
    cb: &mut F,
) -> ControlFlow<()>
where
    F: FnMut(DynCompatibilityViolation) -> ControlFlow<()>,
{
    let interner = DbInterner::new_with(db, Some(trait_.krate(db)), None);
    for super_trait in elaborate::supertrait_def_ids(interner, SolverDefId::TraitId(trait_)).skip(1)
    {
        let super_trait = match super_trait {
            SolverDefId::TraitId(id) => id,
            _ => unreachable!(),
        };
        if db.dyn_compatibility_of_trait(super_trait).is_some() {
            cb(DynCompatibilityViolation::HasNonCompatibleSuperTrait(trait_))?;
        }
    }

    dyn_compatibility_of_trait_with_callback(db, trait_, cb)
}

pub fn dyn_compatibility_of_trait_with_callback<F>(
    db: &dyn HirDatabase,
    trait_: TraitId,
    cb: &mut F,
) -> ControlFlow<()>
where
    F: FnMut(DynCompatibilityViolation) -> ControlFlow<()>,
{
    // Check whether this has a `Sized` bound
    if generics_require_sized_self(db, trait_.into()) {
        cb(DynCompatibilityViolation::SizedSelf)?;
    }

    // Check if there exist bounds that referencing self
    if predicates_reference_self(db, trait_) {
        cb(DynCompatibilityViolation::SelfReferential)?;
    }
    if bounds_reference_self(db, trait_) {
        cb(DynCompatibilityViolation::SelfReferential)?;
    }

    // rustc checks for non-lifetime binders here, but we don't support HRTB yet

    let trait_data = trait_.trait_items(db);
    for (_, assoc_item) in &trait_data.items {
        dyn_compatibility_violation_for_assoc_item(db, trait_, *assoc_item, cb)?;
    }

    ControlFlow::Continue(())
}

pub fn dyn_compatibility_of_trait_query(
    db: &dyn HirDatabase,
    trait_: TraitId,
) -> Option<DynCompatibilityViolation> {
    let mut res = None;
    _ = dyn_compatibility_of_trait_with_callback(db, trait_, &mut |osv| {
        res = Some(osv);
        ControlFlow::Break(())
    });

    res
}

pub fn generics_require_sized_self(db: &dyn HirDatabase, def: GenericDefId) -> bool {
    let krate = def.module(db).krate();
    let Some(sized) = LangItem::Sized.resolve_trait(db, krate) else {
        return false;
    };

    let interner = DbInterner::new_with(db, Some(krate), None);
    let predicates = db.generic_predicates_ns(def);
    elaborate::elaborate(interner, predicates.iter().copied()).any(|pred| {
        match pred.kind().skip_binder() {
            ClauseKind::Trait(trait_pred) => {
                if SolverDefId::TraitId(sized) == trait_pred.def_id()
                    && let rustc_type_ir::TyKind::Param(param_ty) =
                        trait_pred.trait_ref.self_ty().kind()
                    && param_ty.index == 0
                {
                    true
                } else {
                    false
                }
            }
            _ => false,
        }
    })
}

// rustc gathers all the spans that references `Self` for error rendering,
// but we don't have good way to render such locations.
// So, just return single boolean value for existence of such `Self` reference
fn predicates_reference_self(db: &dyn HirDatabase, trait_: TraitId) -> bool {
    db.generic_predicates_ns(trait_.into())
        .iter()
        .any(|pred| predicate_references_self(db, trait_, pred, AllowSelfProjection::No))
}

// Same as the above, `predicates_reference_self`
fn bounds_reference_self(db: &dyn HirDatabase, trait_: TraitId) -> bool {
    let trait_data = trait_.trait_items(db);
    trait_data
        .items
        .iter()
        .filter_map(|(_, it)| match *it {
            AssocItemId::TypeAliasId(id) => Some(associated_ty_item_bounds(db, id)),
            _ => None,
        })
        .any(|bounds| {
            bounds.skip_binder().iter().any(|pred| match pred.skip_binder() {
                rustc_type_ir::ExistentialPredicate::Trait(it) => it.args.iter().any(|arg| {
                    contains_illegal_self_type_reference(db, trait_, &arg, AllowSelfProjection::Yes)
                }),
                rustc_type_ir::ExistentialPredicate::Projection(it) => it.args.iter().any(|arg| {
                    contains_illegal_self_type_reference(db, trait_, &arg, AllowSelfProjection::Yes)
                }),
                rustc_type_ir::ExistentialPredicate::AutoTrait(_) => false,
            })
        })
}

#[derive(Clone, Copy)]
enum AllowSelfProjection {
    Yes,
    No,
}

fn predicate_references_self<'db>(
    db: &'db dyn HirDatabase,
    trait_: TraitId,
    predicate: &Clause<'db>,
    allow_self_projection: AllowSelfProjection,
) -> bool {
    match predicate.kind().skip_binder() {
        ClauseKind::Trait(trait_pred) => trait_pred.trait_ref.args.iter().skip(1).any(|arg| {
            contains_illegal_self_type_reference(db, trait_, &arg, allow_self_projection)
        }),
        ClauseKind::Projection(proj_pred) => {
            proj_pred.projection_term.args.iter().skip(1).any(|arg| {
                contains_illegal_self_type_reference(db, trait_, &arg, allow_self_projection)
            })
        }
        _ => false,
    }
}

fn contains_illegal_self_type_reference<'db, T: rustc_type_ir::TypeVisitable<DbInterner<'db>>>(
    db: &'db dyn HirDatabase,
    trait_: TraitId,
    t: &T,
    allow_self_projection: AllowSelfProjection,
) -> bool {
    struct IllegalSelfTypeVisitor<'db> {
        db: &'db dyn HirDatabase,
        trait_: TraitId,
        super_traits: Option<SmallVec<[TraitId; 4]>>,
        allow_self_projection: AllowSelfProjection,
    }
    impl<'db> rustc_type_ir::TypeVisitor<DbInterner<'db>> for IllegalSelfTypeVisitor<'db> {
        type Result = ControlFlow<()>;

        fn visit_ty(
            &mut self,
            ty: <DbInterner<'db> as rustc_type_ir::Interner>::Ty,
        ) -> Self::Result {
            let interner = DbInterner::new_with(self.db, None, None);
            match ty.kind() {
                rustc_type_ir::TyKind::Param(param) if param.index == 0 => ControlFlow::Break(()),
                rustc_type_ir::TyKind::Param(_) => ControlFlow::Continue(()),
                rustc_type_ir::TyKind::Alias(AliasTyKind::Projection, proj) => match self
                    .allow_self_projection
                {
                    AllowSelfProjection::Yes => {
                        let trait_ = proj.trait_def_id(DbInterner::new_with(self.db, None, None));
                        let trait_ = match trait_ {
                            SolverDefId::TraitId(id) => id,
                            _ => unreachable!(),
                        };
                        if self.super_traits.is_none() {
                            self.super_traits = Some(
                                elaborate::supertrait_def_ids(
                                    interner,
                                    SolverDefId::TraitId(self.trait_),
                                )
                                .map(|super_trait| match super_trait {
                                    SolverDefId::TraitId(id) => id,
                                    _ => unreachable!(),
                                })
                                .collect(),
                            )
                        }
                        if self.super_traits.as_ref().is_some_and(|s| s.contains(&trait_)) {
                            ControlFlow::Continue(())
                        } else {
                            ty.super_visit_with(self)
                        }
                    }
                    AllowSelfProjection::No => ty.super_visit_with(self),
                },
                _ => ty.super_visit_with(self),
            }
        }
    }

    let mut visitor =
        IllegalSelfTypeVisitor { db, trait_, super_traits: None, allow_self_projection };
    t.visit_with(&mut visitor).is_break()
}

fn dyn_compatibility_violation_for_assoc_item<F>(
    db: &dyn HirDatabase,
    trait_: TraitId,
    item: AssocItemId,
    cb: &mut F,
) -> ControlFlow<()>
where
    F: FnMut(DynCompatibilityViolation) -> ControlFlow<()>,
{
    // Any item that has a `Self : Sized` requisite is otherwise
    // exempt from the regulations.
    if generics_require_sized_self(db, item.into()) {
        return ControlFlow::Continue(());
    }

    match item {
        AssocItemId::ConstId(it) => cb(DynCompatibilityViolation::AssocConst(it)),
        AssocItemId::FunctionId(it) => {
            virtual_call_violations_for_method(db, trait_, it, &mut |mvc| {
                cb(DynCompatibilityViolation::Method(it, mvc))
            })
        }
        AssocItemId::TypeAliasId(it) => {
            let def_map = CrateRootModuleId::from(trait_.krate(db)).def_map(db);
            if def_map.is_unstable_feature_enabled(&intern::sym::generic_associated_type_extended) {
                ControlFlow::Continue(())
            } else {
                let generic_params = db.generic_params(item.into());
                if !generic_params.is_empty() {
                    cb(DynCompatibilityViolation::GAT(it))
                } else {
                    ControlFlow::Continue(())
                }
            }
        }
    }
}

fn virtual_call_violations_for_method<F>(
    db: &dyn HirDatabase,
    trait_: TraitId,
    func: FunctionId,
    cb: &mut F,
) -> ControlFlow<()>
where
    F: FnMut(MethodViolationCode) -> ControlFlow<()>,
{
    let func_data = db.function_signature(func);
    if !func_data.has_self_param() {
        cb(MethodViolationCode::StaticMethod)?;
    }

    if func_data.is_async() {
        cb(MethodViolationCode::AsyncFn)?;
    }

    let sig = db.callable_item_signature_ns(func.into());
    if sig
        .skip_binder()
        .inputs()
        .iter()
        .skip(1)
        .any(|ty| contains_illegal_self_type_reference(db, trait_, &ty, AllowSelfProjection::Yes))
    {
        cb(MethodViolationCode::ReferencesSelfInput)?;
    }

    if contains_illegal_self_type_reference(
        db,
        trait_,
        &sig.skip_binder().output(),
        AllowSelfProjection::Yes,
    ) {
        cb(MethodViolationCode::ReferencesSelfOutput)?;
    }

    if !func_data.is_async()
        && let Some(mvc) = contains_illegal_impl_trait_in_trait(db, &sig)
    {
        cb(mvc)?;
    }

    let generic_params = db.generic_params(func.into());
    if generic_params.len_type_or_consts() > 0 {
        cb(MethodViolationCode::Generic)?;
    }

    if func_data.has_self_param() && !receiver_is_dispatchable(db, trait_, func, &sig) {
        cb(MethodViolationCode::UndispatchableReceiver)?;
    }

    let predicates = &*db.generic_predicates_without_parent_ns(func.into());
    for pred in predicates {
        let pred = pred.kind().skip_binder();

        if matches!(pred, ClauseKind::TypeOutlives(_)) {
            continue;
        }

        // Allow `impl AutoTrait` predicates
        let interner = DbInterner::new_with(db, Some(trait_.krate(db)), None);
        if let ClauseKind::Trait(TraitPredicate {
            trait_ref: pred_trait_ref,
            polarity: PredicatePolarity::Positive,
        }) = pred
            && let SolverDefId::TraitId(trait_id) = pred_trait_ref.def_id
            && let trait_data = db.trait_signature(trait_id)
            && trait_data.flags.contains(TraitFlags::AUTO)
            && pred_trait_ref.self_ty()
                == crate::next_solver::Ty::new(
                    interner,
                    rustc_type_ir::TyKind::Param(crate::next_solver::ParamTy { index: 0 }),
                )
        {
            continue;
        }

        if contains_illegal_self_type_reference(db, trait_, &pred, AllowSelfProjection::Yes) {
            cb(MethodViolationCode::WhereClauseReferencesSelf)?;
            break;
        }
    }

    ControlFlow::Continue(())
}

fn receiver_is_dispatchable<'db>(
    db: &dyn HirDatabase,
    trait_: TraitId,
    func: FunctionId,
    sig: &crate::next_solver::EarlyBinder<
        'db,
        crate::next_solver::Binder<'db, rustc_type_ir::FnSig<DbInterner<'db>>>,
    >,
) -> bool {
    let sig = sig.instantiate_identity();

    let interner: DbInterner<'_> = DbInterner::new_with(db, Some(trait_.krate(db)), None);
    let self_param_ty = crate::next_solver::Ty::new(
        interner,
        rustc_type_ir::TyKind::Param(crate::next_solver::ParamTy { index: 0 }),
    );

    // `self: Self` can't be dispatched on, but this is already considered dyn-compatible
    // See rustc's comment on https://github.com/rust-lang/rust/blob/3f121b9461cce02a703a0e7e450568849dfaa074/compiler/rustc_trait_selection/src/traits/object_safety.rs#L433-L437
    if sig.inputs().iter().next().is_some_and(|p| p.skip_binder() == self_param_ty) {
        return true;
    }

    let Some(&receiver_ty) = sig.inputs().skip_binder().as_slice().first() else {
        return false;
    };

    let krate = func.module(db).krate();
    let traits = (
        LangItem::Unsize.resolve_trait(db, krate),
        LangItem::DispatchFromDyn.resolve_trait(db, krate),
    );
    let (Some(unsize_did), Some(dispatch_from_dyn_did)) = traits else {
        return false;
    };

    let meta_sized_did = LangItem::MetaSized.resolve_trait(db, krate);
    let Some(meta_sized_did) = meta_sized_did else {
        return false;
    };

    // Type `U`
    let unsized_self_ty = crate::next_solver::Ty::new_param(interner, u32::MAX, Symbol::empty());
    // `Receiver[Self => U]`
    let unsized_receiver_ty = receiver_for_self_ty(interner, func, receiver_ty, unsized_self_ty);

    let param_env = {
        let generic_predicates = &*db.generic_predicates_ns(func.into());

        // Self: Unsize<U>
        let unsize_predicate = crate::next_solver::TraitRef::new(
            interner,
            SolverDefId::TraitId(unsize_did),
            [self_param_ty, unsized_self_ty],
        );

        // U: Trait<Arg1, ..., ArgN>
        let trait_def_id = SolverDefId::TraitId(trait_);
        let args = GenericArgs::for_item(interner, trait_def_id, |name, index, kind, _| {
            if index == 0 { unsized_self_ty.into() } else { mk_param(interner, index, name, kind) }
        });
        let trait_predicate =
            crate::next_solver::TraitRef::new_from_args(interner, trait_def_id, args);

        let meta_sized_predicate = crate::next_solver::TraitRef::new(
            interner,
            SolverDefId::TraitId(meta_sized_did),
            [unsized_self_ty],
        );

        ParamEnv {
            clauses: Clauses::new_from_iter(
                interner,
                generic_predicates.iter().copied().chain([
                    unsize_predicate.upcast(interner),
                    trait_predicate.upcast(interner),
                    meta_sized_predicate.upcast(interner),
                ]),
            ),
        }
    };

    // Receiver: DispatchFromDyn<Receiver[Self => U]>
    let predicate = crate::next_solver::TraitRef::new(
        interner,
        SolverDefId::TraitId(dispatch_from_dyn_did),
        [receiver_ty, unsized_receiver_ty],
    );
    let goal = crate::next_solver::Goal::new(interner, param_env, predicate);

    let infcx = interner.infer_ctxt().build(TypingMode::non_body_analysis());
    // the receiver is dispatchable iff the obligation holds
    let res = next_trait_solve_in_ctxt(&infcx, goal);
    res.map_or(false, |res| matches!(res.1, rustc_type_ir::solve::Certainty::Yes))
}

fn receiver_for_self_ty<'db>(
    interner: DbInterner<'db>,
    func: FunctionId,
    receiver_ty: crate::next_solver::Ty<'db>,
    self_ty: crate::next_solver::Ty<'db>,
) -> crate::next_solver::Ty<'db> {
    let args = crate::next_solver::GenericArgs::for_item(
        interner,
        SolverDefId::FunctionId(func),
        |name, index, kind, _| {
            if index == 0 { self_ty.into() } else { mk_param(interner, index, name, kind) }
        },
    );

    crate::next_solver::EarlyBinder::bind(receiver_ty).instantiate(interner, args)
}

fn contains_illegal_impl_trait_in_trait<'db>(
    db: &'db dyn HirDatabase,
    sig: &crate::next_solver::EarlyBinder<
        'db,
        crate::next_solver::Binder<'db, rustc_type_ir::FnSig<DbInterner<'db>>>,
    >,
) -> Option<MethodViolationCode> {
    struct OpaqueTypeCollector(FxHashSet<InternedOpaqueTyId>);

    impl<'db> rustc_type_ir::TypeVisitor<DbInterner<'db>> for OpaqueTypeCollector {
        type Result = ControlFlow<()>;

        fn visit_ty(
            &mut self,
            ty: <DbInterner<'db> as rustc_type_ir::Interner>::Ty,
        ) -> Self::Result {
            if let rustc_type_ir::TyKind::Alias(AliasTyKind::Opaque, op) = ty.kind() {
                let id = match op.def_id {
                    SolverDefId::InternedOpaqueTyId(id) => id,
                    _ => unreachable!(),
                };
                self.0.insert(id);
            }
            ty.super_visit_with(self)
        }
    }

    let ret = sig.skip_binder().output();
    let mut visitor = OpaqueTypeCollector(FxHashSet::default());
    _ = ret.visit_with(&mut visitor);

    // Since we haven't implemented RPITIT in proper way like rustc yet,
    // just check whether `ret` contains RPIT for now
    for opaque_ty in visitor.0 {
        let impl_trait_id = db.lookup_intern_impl_trait_id(opaque_ty);
        if matches!(impl_trait_id, ImplTraitId::ReturnTypeImplTrait(..)) {
            return Some(MethodViolationCode::ReferencesImplTraitInTrait);
        }
    }

    None
}

#[cfg(test)]
mod tests;
