use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::{snippet_with_applicability, snippet_with_context};
use clippy_utils::{SpanlessEq, is_lint_allowed, peel_blocks_with_stmt, sym};
use rustc_errors::Applicability;
use rustc_hir::{Closure, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::{Span, SyntaxContext};

use std::borrow::{Borrow, Cow};

use crate::internal_paths;

declare_tool_lint! {
    /// ### What it does
    /// Lints `span_lint_and_then` function calls, where the
    /// closure argument has only one statement and that statement is a method
    /// call to `span_suggestion`, `span_help`, `span_note` (using the same
    /// span), `help` or `note`.
    ///
    /// These usages of `span_lint_and_then` should be replaced with one of the
    /// wrapper functions `span_lint_and_sugg`, span_lint_and_help`, or
    /// `span_lint_and_note`.
    ///
    /// ### Why is this bad?
    /// Using the wrapper `span_lint_and_*` functions, is more
    /// convenient, readable and less error prone.
    ///
    /// ### Example
    /// ```rust,ignore
    /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| {
    ///     diag.span_suggestion(
    ///         expr.span,
    ///         help_msg,
    ///         sugg.to_string(),
    ///         Applicability::MachineApplicable,
    ///     );
    /// });
    /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| {
    ///     diag.span_help(expr.span, help_msg);
    /// });
    /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| {
    ///     diag.help(help_msg);
    /// });
    /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| {
    ///     diag.span_note(expr.span, note_msg);
    /// });
    /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| {
    ///     diag.note(note_msg);
    /// });
    /// ```
    ///
    /// Use instead:
    /// ```rust,ignore
    /// span_lint_and_sugg(
    ///     cx,
    ///     TEST_LINT,
    ///     expr.span,
    ///     lint_msg,
    ///     help_msg,
    ///     sugg.to_string(),
    ///     Applicability::MachineApplicable,
    /// );
    /// span_lint_and_help(cx, TEST_LINT, expr.span, lint_msg, Some(expr.span), help_msg);
    /// span_lint_and_help(cx, TEST_LINT, expr.span, lint_msg, None, help_msg);
    /// span_lint_and_note(cx, TEST_LINT, expr.span, lint_msg, Some(expr.span), note_msg);
    /// span_lint_and_note(cx, TEST_LINT, expr.span, lint_msg, None, note_msg);
    /// ```
    pub clippy::COLLAPSIBLE_SPAN_LINT_CALLS,
    Warn,
    "found collapsible `span_lint_and_then` calls",
    report_in_external_macro: true
}

declare_lint_pass!(CollapsibleCalls => [COLLAPSIBLE_SPAN_LINT_CALLS]);

impl<'tcx> LateLintPass<'tcx> for CollapsibleCalls {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
        if is_lint_allowed(cx, COLLAPSIBLE_SPAN_LINT_CALLS, expr.hir_id) {
            return;
        }

        if let ExprKind::Call(func, [call_cx, call_lint, call_sp, call_msg, call_f]) = expr.kind
            && internal_paths::SPAN_LINT_AND_THEN.matches_path(cx, func)
            && let ExprKind::Closure(&Closure { body, .. }) = call_f.kind
            && let body = cx.tcx.hir_body(body)
            && let only_expr = peel_blocks_with_stmt(body.value)
            && let ExprKind::MethodCall(ps, recv, span_call_args, _) = &only_expr.kind
            && let ExprKind::Path(..) = recv.kind
        {
            let mut app = Applicability::MachineApplicable;
            let expr_ctxt = expr.span.ctxt();
            let and_then_snippets = get_and_then_snippets(
                cx,
                expr_ctxt,
                call_cx.span,
                call_lint.span,
                call_sp.span,
                call_msg.span,
                &mut app,
            );
            let mut sle = SpanlessEq::new(cx).deny_side_effects();
            match ps.ident.name {
                sym::span_suggestion if sle.eq_expr(call_sp, &span_call_args[0]) => {
                    let snippets = span_suggestion_snippets(cx, expr_ctxt, span_call_args, &mut app);
                    suggest_suggestion(cx, expr, &and_then_snippets, &snippets, app);
                },
                sym::span_help if sle.eq_expr(call_sp, &span_call_args[0]) => {
                    let help_snippet =
                        snippet_with_context(cx, span_call_args[1].span, expr_ctxt, r#""...""#, &mut app).0;
                    suggest_help(cx, expr, &and_then_snippets, help_snippet.borrow(), true, app);
                },
                sym::span_note if sle.eq_expr(call_sp, &span_call_args[0]) => {
                    let note_snippet =
                        snippet_with_context(cx, span_call_args[1].span, expr_ctxt, r#""...""#, &mut app).0;
                    suggest_note(cx, expr, &and_then_snippets, note_snippet.borrow(), true, app);
                },
                sym::help => {
                    let help_snippet =
                        snippet_with_context(cx, span_call_args[0].span, expr_ctxt, r#""...""#, &mut app).0;
                    suggest_help(cx, expr, &and_then_snippets, help_snippet.borrow(), false, app);
                },
                sym::note => {
                    let note_snippet =
                        snippet_with_context(cx, span_call_args[0].span, expr_ctxt, r#""...""#, &mut app).0;
                    suggest_note(cx, expr, &and_then_snippets, note_snippet.borrow(), false, app);
                },
                _ => (),
            }
        }
    }
}

struct AndThenSnippets {
    cx: Cow<'static, str>,
    lint: Cow<'static, str>,
    span: Cow<'static, str>,
    msg: Cow<'static, str>,
}

fn get_and_then_snippets(
    cx: &LateContext<'_>,
    expr_ctxt: SyntaxContext,
    cx_span: Span,
    lint_span: Span,
    span_span: Span,
    msg_span: Span,
    app: &mut Applicability,
) -> AndThenSnippets {
    let cx_snippet = snippet_with_applicability(cx, cx_span, "cx", app);
    let lint_snippet = snippet_with_applicability(cx, lint_span, "..", app);
    let span_snippet = snippet_with_applicability(cx, span_span, "span", app);
    let msg_snippet = snippet_with_context(cx, msg_span, expr_ctxt, r#""...""#, app).0;

    AndThenSnippets {
        cx: cx_snippet,
        lint: lint_snippet,
        span: span_snippet,
        msg: msg_snippet,
    }
}

struct SpanSuggestionSnippets {
    help: Cow<'static, str>,
    sugg: Cow<'static, str>,
    applicability: Cow<'static, str>,
}

fn span_suggestion_snippets<'hir>(
    cx: &LateContext<'_>,
    expr_ctxt: SyntaxContext,
    span_call_args: &'hir [Expr<'hir>],
    app: &mut Applicability,
) -> SpanSuggestionSnippets {
    let help_snippet = snippet_with_context(cx, span_call_args[1].span, expr_ctxt, r#""...""#, app).0;
    let sugg_snippet = snippet_with_context(cx, span_call_args[2].span, expr_ctxt, "..", app).0;
    let applicability_snippet =
        snippet_with_applicability(cx, span_call_args[3].span, "Applicability::MachineApplicable", app);

    SpanSuggestionSnippets {
        help: help_snippet,
        sugg: sugg_snippet,
        applicability: applicability_snippet,
    }
}

fn suggest_suggestion(
    cx: &LateContext<'_>,
    expr: &Expr<'_>,
    and_then_snippets: &AndThenSnippets,
    span_suggestion_snippets: &SpanSuggestionSnippets,
    app: Applicability,
) {
    span_lint_and_sugg(
        cx,
        COLLAPSIBLE_SPAN_LINT_CALLS,
        expr.span,
        "this call is collapsible",
        "collapse into",
        format!(
            "span_lint_and_sugg({}, {}, {}, {}, {}, {}, {})",
            and_then_snippets.cx,
            and_then_snippets.lint,
            and_then_snippets.span,
            and_then_snippets.msg,
            span_suggestion_snippets.help,
            span_suggestion_snippets.sugg,
            span_suggestion_snippets.applicability
        ),
        app,
    );
}

fn suggest_help(
    cx: &LateContext<'_>,
    expr: &Expr<'_>,
    and_then_snippets: &AndThenSnippets,
    help: &str,
    with_span: bool,
    app: Applicability,
) {
    let option_span = if with_span {
        format!("Some({})", and_then_snippets.span)
    } else {
        "None".to_string()
    };

    span_lint_and_sugg(
        cx,
        COLLAPSIBLE_SPAN_LINT_CALLS,
        expr.span,
        "this call is collapsible",
        "collapse into",
        format!(
            "span_lint_and_help({}, {}, {}, {}, {}, {help})",
            and_then_snippets.cx, and_then_snippets.lint, and_then_snippets.span, and_then_snippets.msg, &option_span,
        ),
        app,
    );
}

fn suggest_note(
    cx: &LateContext<'_>,
    expr: &Expr<'_>,
    and_then_snippets: &AndThenSnippets,
    note: &str,
    with_span: bool,
    app: Applicability,
) {
    let note_span = if with_span {
        format!("Some({})", and_then_snippets.span)
    } else {
        "None".to_string()
    };

    span_lint_and_sugg(
        cx,
        COLLAPSIBLE_SPAN_LINT_CALLS,
        expr.span,
        "this call is collapsible",
        "collapse into",
        format!(
            "span_lint_and_note({}, {}, {}, {}, {note_span}, {note})",
            and_then_snippets.cx, and_then_snippets.lint, and_then_snippets.span, and_then_snippets.msg,
        ),
        app,
    );
}
