use rustc_data_structures::fx::FxHashSet;
use rustc_data_structures::unord::UnordSet;
use rustc_hir::def_id::DefId;
use rustc_span::Span;
use smallvec::{SmallVec, smallvec};
use tracing::{debug, instrument};

use crate::ty::{self, Ty, TyCtxt, Unnormalized};

/// An additional filter to exclude well-known types from the ecosystem
/// because their drops are trivial.
/// This returns additional types to check if the drops are delegated to those.
/// A typical example is `hashbrown::HashMap<K, V>`, whose drop is delegated to `K` and `V`.
fn true_significant_drop_ty<'tcx>(
    tcx: TyCtxt<'tcx>,
    ty: Ty<'tcx>,
) -> Option<SmallVec<[Ty<'tcx>; 2]>> {
    if let ty::Adt(def, args) = ty.kind() {
        let mut did = def.did();
        let mut name_rev = vec![];
        loop {
            let key = tcx.def_key(did);

            match key.disambiguated_data.data {
                rustc_hir::definitions::DefPathData::CrateRoot => {
                    name_rev.push(tcx.crate_name(did.krate));
                }
                rustc_hir::definitions::DefPathData::TypeNs(symbol) => {
                    name_rev.push(symbol);
                }
                _ => return None,
            }
            if let Some(parent) = key.parent {
                did = DefId { krate: did.krate, index: parent };
            } else {
                break;
            }
        }
        let name_str: Vec<_> = name_rev.iter().rev().map(|x| x.as_str()).collect();
        debug!(?name_str);
        match name_str[..] {
            // These are the types from Rust core ecosystem
            ["syn" | "proc_macro2", ..]
            | ["core" | "std", "task", "LocalWaker" | "Waker"]
            | ["core" | "std", "task", "wake", "LocalWaker" | "Waker"] => Some(smallvec![]),
            // These are important types from Rust ecosystem
            ["tracing", "instrument", "Instrumented"] | ["bytes", "Bytes"] => Some(smallvec![]),
            ["hashbrown", "raw", "RawTable" | "RawIntoIter"] => {
                if let [ty, ..] = &***args
                    && let Some(ty) = ty.as_type()
                {
                    Some(smallvec![ty])
                } else {
                    None
                }
            }
            ["hashbrown", "raw", "RawDrain"] => {
                if let [_, ty, ..] = &***args
                    && let Some(ty) = ty.as_type()
                {
                    Some(smallvec![ty])
                } else {
                    None
                }
            }
            _ => None,
        }
    } else {
        None
    }
}

/// Returns the list of types with a "potentially significant" that may be dropped
/// by dropping a value of type `ty`.
#[instrument(level = "trace", skip(tcx, typing_env))]
pub fn extract_component_raw<'tcx>(
    tcx: TyCtxt<'tcx>,
    typing_env: ty::TypingEnv<'tcx>,
    ty: Ty<'tcx>,
    ty_seen: &mut UnordSet<Ty<'tcx>>,
) -> SmallVec<[Ty<'tcx>; 4]> {
    // Droppiness does not depend on regions, so let us erase them.
    let ty = tcx.try_normalize_erasing_regions(typing_env, Unnormalized::new_wip(ty)).unwrap_or(ty);

    let tys = tcx.list_significant_drop_tys(typing_env.as_query_input(ty));
    debug!(?ty, "components");
    let mut out_tys = smallvec![];
    for ty in tys {
        if let Some(tys) = true_significant_drop_ty(tcx, ty) {
            // Some types can be further opened up because the drop is simply delegated
            for ty in tys {
                if ty_seen.insert(ty) {
                    out_tys.extend(extract_component_raw(tcx, typing_env, ty, ty_seen));
                }
            }
        } else {
            if ty_seen.insert(ty) {
                out_tys.push(ty);
            }
        }
    }
    out_tys
}

#[instrument(level = "trace", skip(tcx, typing_env))]
pub fn extract_component_with_significant_dtor<'tcx>(
    tcx: TyCtxt<'tcx>,
    typing_env: ty::TypingEnv<'tcx>,
    ty: Ty<'tcx>,
) -> SmallVec<[Ty<'tcx>; 4]> {
    let mut tys = extract_component_raw(tcx, typing_env, ty, &mut Default::default());
    let mut deduplicate = FxHashSet::default();
    tys.retain(|oty| deduplicate.insert(*oty));
    tys.into_iter().collect()
}

/// Extract the span of the custom destructor of a type
/// especially the span of the `impl Drop` header or its entire block
/// when we are working with current local crate.
#[instrument(level = "trace", skip(tcx))]
pub fn ty_dtor_span<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option<Span> {
    match *ty.kind() {
        ty::Bool
        | ty::Char
        | ty::Int(_)
        | ty::Uint(_)
        | ty::Float(_)
        | ty::Error(_)
        | ty::Str
        | ty::Never
        | ty::RawPtr(_, _)
        | ty::Ref(_, _, _)
        | ty::FnPtr(_, _)
        | ty::Tuple(_)
        | ty::Dynamic(_, _)
        | ty::Alias(_)
        | ty::Bound(_, _)
        | ty::Pat(_, _)
        | ty::Placeholder(_)
        | ty::Infer(_)
        | ty::Slice(_)
        | ty::Array(_, _)
        | ty::UnsafeBinder(_) => None,

        ty::Adt(adt_def, _) => {
            if let Some(dtor) = tcx.adt_destructor(adt_def.did()) {
                Some(tcx.def_span(tcx.parent(dtor.did)))
            } else {
                Some(tcx.def_span(adt_def.did()))
            }
        }
        ty::Coroutine(did, _)
        | ty::CoroutineWitness(did, _)
        | ty::CoroutineClosure(did, _)
        | ty::Closure(did, _)
        | ty::FnDef(did, _)
        | ty::Foreign(did) => Some(tcx.def_span(did)),
        ty::Param(_) => None,
    }
}
