use crate::internal_paths;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::paths::{PathNS, lookup_path};
use clippy_utils::peel_ref_operators;
use clippy_utils::res::MaybeQPath;
use rustc_hir::def_id::DefId;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_lint_defs::{declare_lint_pass, declare_tool_lint};
use rustc_middle::mir::ConstValue;
use rustc_span::symbol::Symbol;

declare_tool_lint! {
    /// ### What it does
    /// Checks for usage of def paths when a diagnostic item or a `LangItem` could be used.
    ///
    /// ### Why is this bad?
    /// The path for an item is subject to change and is less efficient to look up than a
    /// diagnostic item or a `LangItem`.
    ///
    /// ### Example
    /// ```rust,ignore
    /// pub static VEC: PathLookup = path!(alloc::vec::Vec);
    ///
    /// VEC.contains_ty(cx, ty)
    /// ```
    ///
    /// Use instead:
    /// ```rust,ignore
    /// ty.is_diag_item(cx, sym::Vec)
    /// ```
    pub clippy::UNNECESSARY_DEF_PATH,
    Warn,
    "using a def path when a diagnostic item or a `LangItem` is available",
    report_in_external_macro: true
}

declare_lint_pass!(UnnecessaryDefPath => [UNNECESSARY_DEF_PATH]);

impl<'tcx> LateLintPass<'tcx> for UnnecessaryDefPath {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
        if let ExprKind::Call(ctor, [_, path]) = expr.kind
            && internal_paths::PATH_LOOKUP_NEW.matches_path(cx, ctor)
            && let ExprKind::Array(segments) = peel_ref_operators(cx, path).kind
            && let Some(macro_id) = expr.span.ctxt().outer_expn_data().macro_def_id
        {
            let ns = match cx.tcx.item_name(macro_id).as_str() {
                "type_path" => PathNS::Type,
                "value_path" => PathNS::Value,
                "macro_path" => PathNS::Macro,
                _ => unreachable!(),
            };

            let path: Vec<Symbol> = segments
                .iter()
                .map(|segment| {
                    if let Some(const_def_id) = segment.res(cx).opt_def_id()
                        && let Ok(ConstValue::Scalar(value)) = cx.tcx.const_eval_poly(const_def_id)
                        && let Some(value) = value.to_u32().discard_err()
                    {
                        Symbol::new(value)
                    } else {
                        panic!("failed to resolve path {:?}", expr.span);
                    }
                })
                .collect();

            for def_id in lookup_path(cx.tcx, ns, &path) {
                if let Some(name) = cx.tcx.get_diagnostic_name(def_id) {
                    span_lint_and_then(
                        cx,
                        UNNECESSARY_DEF_PATH,
                        expr.span.source_callsite(),
                        format!("a diagnostic name exists for this path: sym::{name}"),
                        |diag| {
                            diag.help(
                                "remove the `PathLookup` and use utilities such as `cx.tcx.is_diagnostic_item` instead",
                            );
                            diag.help("see also https://doc.rust-lang.org/nightly/nightly-rustc/?search=diag&filter-crate=clippy_utils");
                        },
                    );
                } else if let Some(item_name) = get_lang_item_name(cx, def_id) {
                    span_lint_and_then(
                        cx,
                        UNNECESSARY_DEF_PATH,
                        expr.span.source_callsite(),
                        format!("a language item exists for this path: LangItem::{item_name}"),
                        |diag| {
                            diag.help("remove the `PathLookup` and use utilities such as `cx.tcx.lang_items` instead");
                            diag.help("see also https://doc.rust-lang.org/nightly/nightly-rustc/?search=lang&filter-crate=clippy_utils");
                        },
                    );
                }
            }
        }
    }
}

fn get_lang_item_name(cx: &LateContext<'_>, def_id: DefId) -> Option<&'static str> {
    if let Some((lang_item, _)) = cx.tcx.lang_items().iter().find(|(_, id)| *id == def_id) {
        Some(lang_item.variant_name())
    } else {
        None
    }
}
