use clippy_config::Conf;
use clippy_config::types::{DisallowedPath, create_disallowed_map};
use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then};
use clippy_utils::macros::macro_backtrace;
use clippy_utils::paths::PathNS;
use rustc_data_structures::fx::FxHashSet;
use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefIdMap;
use rustc_hir::{
    AmbigArg, Expr, ExprKind, ForeignItem, HirId, ImplItem, ImplItemImplKind, Item, ItemKind, OwnerId, Pat, Path, Stmt,
    TraitItem, Ty,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::TyCtxt;
use rustc_session::impl_lint_pass;
use rustc_span::{ExpnId, MacroKind, Span};

use crate::utils::attr_collector::AttrStorage;

declare_clippy_lint! {
    /// ### What it does
    /// Denies the configured macros in clippy.toml
    ///
    /// Note: Even though this lint is warn-by-default, it will only trigger if
    /// macros are defined in the clippy.toml file.
    ///
    /// ### Why is this bad?
    /// Some macros are undesirable in certain contexts, and it's beneficial to
    /// lint for them as needed.
    ///
    /// ### Example
    /// An example clippy.toml configuration:
    /// ```toml
    /// # clippy.toml
    /// disallowed-macros = [
    ///     # Can use a string as the path of the disallowed macro.
    ///     "std::print",
    ///     # Can also use an inline table with a `path` key.
    ///     { path = "std::println" },
    ///     # When using an inline table, can add a `reason` for why the macro
    ///     # is disallowed.
    ///     { path = "serde::Serialize", reason = "no serializing" },
    ///     # This would normally error if the path is incorrect, but with `allow-invalid` = `true`,
    ///     # it will be silently ignored
    ///     { path = "std::invalid_macro", reason = "use alternative instead", allow-invalid = true }
    /// ]
    /// ```
    /// ```no_run
    /// use serde::Serialize;
    ///
    /// println!("warns");
    ///
    /// // The diagnostic will contain the message "no serializing"
    /// #[derive(Serialize)]
    /// struct Data {
    ///     name: String,
    ///     value: usize,
    /// }
    /// ```
    #[clippy::version = "1.66.0"]
    pub DISALLOWED_MACROS,
    style,
    "use of a disallowed macro"
}

pub struct DisallowedMacros {
    disallowed: DefIdMap<(&'static str, &'static DisallowedPath)>,
    seen: FxHashSet<ExpnId>,
    // Track the most recently seen node that can have a `derive` attribute.
    // Needed to use the correct lint level.
    derive_src: Option<OwnerId>,

    // When a macro is disallowed in an early pass, it's stored
    // and emitted during the late pass. This happens for attributes.
    early_macro_cache: AttrStorage,
}

impl DisallowedMacros {
    pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf, early_macro_cache: AttrStorage) -> Self {
        let (disallowed, _) = create_disallowed_map(
            tcx,
            &conf.disallowed_macros,
            PathNS::Macro,
            |def_kind| matches!(def_kind, DefKind::Macro(_)),
            "macro",
            false,
        );
        Self {
            disallowed,
            seen: FxHashSet::default(),
            derive_src: None,
            early_macro_cache,
        }
    }

    fn check(&mut self, cx: &LateContext<'_>, span: Span, derive_src: Option<OwnerId>) {
        if self.disallowed.is_empty() {
            return;
        }

        for mac in macro_backtrace(span) {
            if !self.seen.insert(mac.expn) {
                return;
            }

            if let Some(&(path, disallowed_path)) = self.disallowed.get(&mac.def_id) {
                let msg = format!("use of a disallowed macro `{path}`");
                let add_note = disallowed_path.diag_amendment(mac.span);
                if matches!(mac.kind, MacroKind::Derive)
                    && let Some(derive_src) = derive_src
                {
                    span_lint_hir_and_then(
                        cx,
                        DISALLOWED_MACROS,
                        cx.tcx.local_def_id_to_hir_id(derive_src.def_id),
                        mac.span,
                        msg,
                        add_note,
                    );
                } else {
                    span_lint_and_then(cx, DISALLOWED_MACROS, mac.span, msg, add_note);
                }
            }
        }
    }
}

impl_lint_pass!(DisallowedMacros => [DISALLOWED_MACROS]);

impl LateLintPass<'_> for DisallowedMacros {
    fn check_crate(&mut self, cx: &LateContext<'_>) {
        // once we check a crate in the late pass we can emit the early pass lints
        if let Some(attr_spans) = self.early_macro_cache.clone().0.get() {
            for span in attr_spans {
                self.check(cx, *span, None);
            }
        }
    }

    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        self.check(cx, expr.span, None);
        // `$t + $t` can have the context of $t, check also the span of the binary operator
        if let ExprKind::Binary(op, ..) = expr.kind {
            self.check(cx, op.span, None);
        }
    }

    fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &Stmt<'_>) {
        self.check(cx, stmt.span, None);
    }

    fn check_ty(&mut self, cx: &LateContext<'_>, ty: &Ty<'_, AmbigArg>) {
        self.check(cx, ty.span, None);
    }

    fn check_pat(&mut self, cx: &LateContext<'_>, pat: &Pat<'_>) {
        self.check(cx, pat.span, None);
    }

    fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
        self.check(cx, item.span, self.derive_src);
        self.check(cx, item.vis_span, None);

        if matches!(
            item.kind,
            ItemKind::Struct(..) | ItemKind::Enum(..) | ItemKind::Union(..)
        ) && macro_backtrace(item.span).all(|m| !matches!(m.kind, MacroKind::Derive))
        {
            self.derive_src = Some(item.owner_id);
        }
    }

    fn check_foreign_item(&mut self, cx: &LateContext<'_>, item: &ForeignItem<'_>) {
        self.check(cx, item.span, None);
        self.check(cx, item.vis_span, None);
    }

    fn check_impl_item(&mut self, cx: &LateContext<'_>, item: &ImplItem<'_>) {
        self.check(cx, item.span, None);
        if let ImplItemImplKind::Inherent { vis_span, .. } = item.impl_kind {
            self.check(cx, vis_span, None);
        }
    }

    fn check_trait_item(&mut self, cx: &LateContext<'_>, item: &TraitItem<'_>) {
        self.check(cx, item.span, None);
    }

    fn check_path(&mut self, cx: &LateContext<'_>, path: &Path<'_>, _: HirId) {
        self.check(cx, path.span, None);
    }
}
