use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::{is_in_test, sym};
use clippy_utils::macros::{MacroCall, macro_backtrace};
use clippy_utils::source::snippet_with_applicability;
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_hir::{Closure, ClosureKind, CoroutineKind, Expr, ExprKind, LetStmt, LocalSource, Node, Stmt, StmtKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::impl_lint_pass;
use rustc_span::{Span, SyntaxContext};

declare_clippy_lint! {
    /// ### What it does
    /// Checks for usage of the [`dbg!`](https://doc.rust-lang.org/std/macro.dbg.html) macro.
    ///
    /// ### Why restrict this?
    /// The `dbg!` macro is intended as a debugging tool. It should not be present in released
    /// software or committed to a version control system.
    ///
    /// ### Example
    /// ```rust,ignore
    /// dbg!(true)
    /// ```
    ///
    /// Use instead:
    /// ```rust,ignore
    /// true
    /// ```
    #[clippy::version = "1.34.0"]
    pub DBG_MACRO,
    restriction,
    "`dbg!` macro is intended as a debugging tool"
}

pub struct DbgMacro {
    allow_dbg_in_tests: bool,
    /// Tracks the `dbg!` macro callsites that are already checked.
    checked_dbg_call_site: FxHashSet<Span>,
    /// Tracks the previous `SyntaxContext`, to avoid walking the same context chain.
    prev_ctxt: SyntaxContext,
}

impl_lint_pass!(DbgMacro => [DBG_MACRO]);

impl DbgMacro {
    pub fn new(conf: &'static Conf) -> Self {
        DbgMacro {
            allow_dbg_in_tests: conf.allow_dbg_in_tests,
            checked_dbg_call_site: FxHashSet::default(),
            prev_ctxt: SyntaxContext::root(),
        }
    }
}

impl LateLintPass<'_> for DbgMacro {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        let cur_syntax_ctxt = expr.span.ctxt();

        if cur_syntax_ctxt != self.prev_ctxt &&
            let Some(macro_call) = first_dbg_macro_in_expansion(cx, expr.span) &&
            !macro_call.span.in_external_macro(cx.sess().source_map()) &&
            // avoids exprs generated by the desugaring of coroutines
            !is_coroutine_desugar(expr) &&
            self.checked_dbg_call_site.insert(macro_call.span) &&
            // allows `dbg!` in test code if allow-dbg-in-test is set to true in clippy.toml
            !(self.allow_dbg_in_tests && is_in_test(cx.tcx, expr.hir_id))
        {
            self.prev_ctxt = cur_syntax_ctxt;

            span_lint_and_then(
                cx,
                DBG_MACRO,
                macro_call.span,
                "the `dbg!` macro is intended as a debugging tool",
                |diag| {
                    let mut applicability = Applicability::MachineApplicable;
                    let (sugg_span, suggestion) = match is_async_move_desugar(expr)
                        .unwrap_or(expr)
                        .peel_drop_temps()
                        .kind
                    {
                        // dbg!()
                        ExprKind::Block(..) => {
                            // If the `dbg!` macro is a "free" statement and not contained within other expressions,
                            // remove the whole statement.
                            if let Node::Stmt(_) = cx.tcx.parent_hir_node(expr.hir_id)
                                && let Some(semi_span) = cx.sess().source_map().mac_call_stmt_semi_span(macro_call.span)
                            {
                                (macro_call.span.to(semi_span), String::new())
                            } else {
                                (macro_call.span, String::from("()"))
                            }
                        },
                        // dbg!(1)
                        ExprKind::Match(val, ..) => (
                            macro_call.span,
                            snippet_with_applicability(cx, val.span.source_callsite(), "..", &mut applicability)
                                .to_string(),
                        ),
                        // dbg!(2, 3)
                        ExprKind::Tup(
                            [
                                Expr {
                                    kind: ExprKind::Match(first, ..),
                                    ..
                                },
                                ..,
                                Expr {
                                    kind: ExprKind::Match(last, ..),
                                    ..
                                },
                            ],
                        ) => {
                            let snippet = snippet_with_applicability(
                                cx,
                                first.span.source_callsite().to(last.span.source_callsite()),
                                "..",
                                &mut applicability,
                            );
                            (macro_call.span, format!("({snippet})"))
                        },
                        _ => unreachable!(),
                    };

                    diag.span_suggestion(
                        sugg_span,
                        "remove the invocation before committing it to a version control system",
                        suggestion,
                        applicability,
                    );
                },
            );
        }
    }

    fn check_crate_post(&mut self, _: &LateContext<'_>) {
        self.checked_dbg_call_site = FxHashSet::default();
    }
}

fn is_coroutine_desugar(expr: &Expr<'_>) -> bool {
    matches!(
        expr.kind,
        ExprKind::Closure(Closure {
            kind: ClosureKind::Coroutine(CoroutineKind::Desugared(..)) | ClosureKind::CoroutineClosure(..),
            ..
        })
    )
}

fn is_async_move_desugar<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
    if let ExprKind::Block(block, _) = expr.kind
        && let [
            Stmt {
                kind:
                    StmtKind::Let(LetStmt {
                        source: LocalSource::AsyncFn,
                        ..
                    }),
                ..
            },
        ] = block.stmts
    {
        return block.expr;
    }

    None
}

fn first_dbg_macro_in_expansion(cx: &LateContext<'_>, span: Span) -> Option<MacroCall> {
    macro_backtrace(span).find(|mc| cx.tcx.is_diagnostic_item(sym::dbg_macro, mc.def_id))
}
