//! Utilities for computing drop info about types.

use chalk_ir::cast::Cast;
use hir_def::AdtId;
use hir_def::lang_item::LangItem;
use hir_def::signatures::StructFlags;
use stdx::never;
use triomphe::Arc;

use crate::{
    AliasTy, Canonical, CanonicalVarKinds, ConcreteConst, ConstScalar, ConstValue, InEnvironment,
    Interner, ProjectionTy, TraitEnvironment, Ty, TyBuilder, TyKind, db::HirDatabase,
    method_resolution::TyFingerprint,
};

fn has_destructor(db: &dyn HirDatabase, adt: AdtId) -> bool {
    let module = match adt {
        AdtId::EnumId(id) => db.lookup_intern_enum(id).container,
        AdtId::StructId(id) => db.lookup_intern_struct(id).container,
        AdtId::UnionId(id) => db.lookup_intern_union(id).container,
    };
    let Some(drop_trait) = LangItem::Drop.resolve_trait(db, module.krate()) else {
        return false;
    };
    let impls = match module.containing_block() {
        Some(block) => match db.trait_impls_in_block(block) {
            Some(it) => it,
            None => return false,
        },
        None => db.trait_impls_in_crate(module.krate()),
    };
    impls.for_trait_and_self_ty(drop_trait, TyFingerprint::Adt(adt)).next().is_some()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DropGlue {
    // Order of variants is important.
    None,
    /// May have a drop glue if some type parameter has it.
    ///
    /// For the compiler this is considered as a positive result, IDE distinguishes this from "yes".
    DependOnParams,
    HasDropGlue,
}

pub(crate) fn has_drop_glue(db: &dyn HirDatabase, ty: Ty, env: Arc<TraitEnvironment>) -> DropGlue {
    match ty.kind(Interner) {
        TyKind::Adt(adt, subst) => {
            if has_destructor(db, adt.0) {
                return DropGlue::HasDropGlue;
            }
            match adt.0 {
                AdtId::StructId(id) => {
                    if db.struct_signature(id).flags.contains(StructFlags::IS_MANUALLY_DROP) {
                        return DropGlue::None;
                    }
                    db.field_types(id.into())
                        .iter()
                        .map(|(_, field_ty)| {
                            db.has_drop_glue(
                                field_ty.clone().substitute(Interner, subst),
                                env.clone(),
                            )
                        })
                        .max()
                        .unwrap_or(DropGlue::None)
                }
                // Unions cannot have fields with destructors.
                AdtId::UnionId(_) => DropGlue::None,
                AdtId::EnumId(id) => id
                    .enum_variants(db)
                    .variants
                    .iter()
                    .map(|&(variant, _, _)| {
                        db.field_types(variant.into())
                            .iter()
                            .map(|(_, field_ty)| {
                                db.has_drop_glue(
                                    field_ty.clone().substitute(Interner, subst),
                                    env.clone(),
                                )
                            })
                            .max()
                            .unwrap_or(DropGlue::None)
                    })
                    .max()
                    .unwrap_or(DropGlue::None),
            }
        }
        TyKind::Tuple(_, subst) => subst
            .iter(Interner)
            .map(|ty| ty.assert_ty_ref(Interner))
            .map(|ty| db.has_drop_glue(ty.clone(), env.clone()))
            .max()
            .unwrap_or(DropGlue::None),
        TyKind::Array(ty, len) => {
            if let ConstValue::Concrete(ConcreteConst { interned: ConstScalar::Bytes(len, _) }) =
                &len.data(Interner).value
            {
                match (&**len).try_into() {
                    Ok(len) => {
                        let len = usize::from_le_bytes(len);
                        if len == 0 {
                            // Arrays of size 0 don't have drop glue.
                            return DropGlue::None;
                        }
                    }
                    Err(_) => {
                        never!("const array size with non-usize len");
                    }
                }
            }
            db.has_drop_glue(ty.clone(), env)
        }
        TyKind::Slice(ty) => db.has_drop_glue(ty.clone(), env),
        TyKind::Closure(closure_id, subst) => {
            let owner = db.lookup_intern_closure((*closure_id).into()).0;
            let infer = db.infer(owner);
            let (captures, _) = infer.closure_info(closure_id);
            let env = db.trait_environment_for_body(owner);
            captures
                .iter()
                .map(|capture| db.has_drop_glue(capture.ty(subst), env.clone()))
                .max()
                .unwrap_or(DropGlue::None)
        }
        // FIXME: Handle coroutines.
        TyKind::Coroutine(..) | TyKind::CoroutineWitness(..) => DropGlue::None,
        TyKind::Ref(..)
        | TyKind::Raw(..)
        | TyKind::FnDef(..)
        | TyKind::Str
        | TyKind::Never
        | TyKind::Scalar(_)
        | TyKind::Function(_)
        | TyKind::Foreign(_)
        | TyKind::Error => DropGlue::None,
        TyKind::Dyn(_) => DropGlue::HasDropGlue,
        TyKind::AssociatedType(assoc_type_id, subst) => projection_has_drop_glue(
            db,
            env,
            ProjectionTy { associated_ty_id: *assoc_type_id, substitution: subst.clone() },
            ty,
        ),
        TyKind::Alias(AliasTy::Projection(projection)) => {
            projection_has_drop_glue(db, env, projection.clone(), ty)
        }
        TyKind::OpaqueType(..) | TyKind::Alias(AliasTy::Opaque(_)) => {
            if is_copy(db, ty, env) {
                DropGlue::None
            } else {
                DropGlue::HasDropGlue
            }
        }
        TyKind::Placeholder(_) | TyKind::BoundVar(_) => {
            if is_copy(db, ty, env) {
                DropGlue::None
            } else {
                DropGlue::DependOnParams
            }
        }
        TyKind::InferenceVar(..) => unreachable!("inference vars shouldn't exist out of inference"),
    }
}

fn projection_has_drop_glue(
    db: &dyn HirDatabase,
    env: Arc<TraitEnvironment>,
    projection: ProjectionTy,
    ty: Ty,
) -> DropGlue {
    let normalized = db.normalize_projection(projection, env.clone());
    match normalized.kind(Interner) {
        TyKind::Alias(AliasTy::Projection(_)) | TyKind::AssociatedType(..) => {
            if is_copy(db, ty, env) { DropGlue::None } else { DropGlue::DependOnParams }
        }
        _ => db.has_drop_glue(normalized, env),
    }
}

fn is_copy(db: &dyn HirDatabase, ty: Ty, env: Arc<TraitEnvironment>) -> bool {
    let Some(copy_trait) = LangItem::Copy.resolve_trait(db, env.krate) else {
        return false;
    };
    let trait_ref = TyBuilder::trait_ref(db, copy_trait).push(ty).build();
    let goal = Canonical {
        value: InEnvironment::new(&env.env, trait_ref.cast(Interner)),
        binders: CanonicalVarKinds::empty(Interner),
    };
    db.trait_solve(env.krate, env.block, goal).certain()
}

pub(crate) fn has_drop_glue_cycle_result(
    _db: &dyn HirDatabase,
    _ty: Ty,
    _env: Arc<TraitEnvironment>,
) -> DropGlue {
    DropGlue::None
}
