use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::macros::{FormatArgsStorage, format_args_inputs_span, root_macro_call_first_node};
use clippy_utils::res::MaybeDef;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::visitors::for_each_expr;
use clippy_utils::{contains_return, is_inside_always_const_context, peel_blocks};
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_lint::LateContext;
use rustc_span::Span;
use rustc_span::symbol::sym;
use std::borrow::Cow;
use std::ops::ControlFlow;

use super::EXPECT_FUN_CALL;

/// Checks for the `EXPECT_FUN_CALL` lint.
pub(super) fn check<'tcx>(
    cx: &LateContext<'tcx>,
    format_args_storage: &FormatArgsStorage,
    expr: &hir::Expr<'_>,
    method_span: Span,
    receiver: &'tcx hir::Expr<'tcx>,
    arg: &'tcx hir::Expr<'tcx>,
) {
    let arg_root = get_arg_root(cx, arg);
    if contains_call(cx, arg_root) && !contains_return(arg_root) {
        let receiver_type = cx.typeck_results().expr_ty_adjusted(receiver);
        let closure_args = if receiver_type.is_diag_item(cx, sym::Option) {
            "||"
        } else if receiver_type.is_diag_item(cx, sym::Result) {
            "|_|"
        } else {
            return;
        };

        let span_replace_word = method_span.with_hi(expr.span.hi());

        let mut applicability = Applicability::MachineApplicable;

        // Special handling for `format!` as arg_root
        if let Some(macro_call) = root_macro_call_first_node(cx, arg_root) {
            if cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id)
                && let Some(format_args) = format_args_storage.get(cx, arg_root, macro_call.expn)
            {
                let span = format_args_inputs_span(format_args);
                let sugg = snippet_with_applicability(cx, span, "..", &mut applicability);
                span_lint_and_sugg(
                    cx,
                    EXPECT_FUN_CALL,
                    span_replace_word,
                    "function call inside of `expect`",
                    "try",
                    format!("unwrap_or_else({closure_args} panic!({sugg}))"),
                    applicability,
                );
            }
            return;
        }

        let arg_root_snippet: Cow<'_, _> = snippet_with_applicability(cx, arg_root.span, "..", &mut applicability);

        span_lint_and_sugg(
            cx,
            EXPECT_FUN_CALL,
            span_replace_word,
            "function call inside of `expect`",
            "try",
            format!("unwrap_or_else({closure_args} panic!(\"{{}}\", {arg_root_snippet}))"),
            applicability,
        );
    }
}

/// Strip `{}`, `&`, `as_ref()` and `as_str()` off `arg` until we're left with either a `String` or
/// `&str`
fn get_arg_root<'a>(cx: &LateContext<'_>, arg: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> {
    let mut arg_root = peel_blocks(arg);
    loop {
        arg_root = match &arg_root.kind {
            hir::ExprKind::AddrOf(hir::BorrowKind::Ref, _, expr) => expr,
            hir::ExprKind::MethodCall(method_name, receiver, [], ..) => {
                if (method_name.ident.name == sym::as_str || method_name.ident.name == sym::as_ref) && {
                    let arg_type = cx.typeck_results().expr_ty(receiver);
                    let base_type = arg_type.peel_refs();
                    base_type.is_str() || base_type.is_lang_item(cx, hir::LangItem::String)
                } {
                    receiver
                } else {
                    break;
                }
            },
            _ => break,
        };
    }
    arg_root
}

fn contains_call<'a>(cx: &LateContext<'a>, arg: &'a hir::Expr<'a>) -> bool {
    for_each_expr(cx, arg, |expr| {
        if matches!(expr.kind, hir::ExprKind::MethodCall { .. } | hir::ExprKind::Call { .. })
            && !is_inside_always_const_context(cx.tcx, expr.hir_id)
        {
            ControlFlow::Break(())
        } else {
            ControlFlow::Continue(())
        }
    })
    .is_some()
}
