use super::utils::derefs_to_slice;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::get_parent_expr;
use clippy_utils::source::snippet_with_applicability;
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_lint::LateContext;
use rustc_span::sym;

use super::GET_UNWRAP;

pub(super) fn check<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &hir::Expr<'_>,
    recv: &'tcx hir::Expr<'tcx>,
    get_arg: &'tcx hir::Expr<'_>,
    is_mut: bool,
) {
    // Note: we don't want to lint `get_mut().unwrap` for `HashMap` or `BTreeMap`,
    // because they do not implement `IndexMut`
    let expr_ty = cx.typeck_results().expr_ty(recv);
    let caller_type = if derefs_to_slice(cx, recv, expr_ty).is_some() {
        "slice"
    } else {
        match expr_ty
            .ty_adt_def()
            .and_then(|def| cx.tcx.get_diagnostic_name(def.did()))
        {
            Some(sym::Vec) => "Vec",
            Some(sym::VecDeque) => "VecDeque",
            Some(sym::HashMap) if !is_mut => "HashMap",
            Some(sym::BTreeMap) if !is_mut => "BTreeMap",
            _ => return, // caller is not a type that we want to lint
        }
    };

    let mut span = expr.span;

    // Handle the case where the result is immediately dereferenced,
    // either directly be the user, or as a result of a method call or the like
    // by not requiring an explicit reference
    let needs_ref = if let Some(parent) = get_parent_expr(cx, expr)
        && let hir::ExprKind::Unary(hir::UnOp::Deref, _)
        | hir::ExprKind::MethodCall(..)
        | hir::ExprKind::Field(..)
        | hir::ExprKind::Index(..) = parent.kind
    {
        if let hir::ExprKind::Unary(hir::UnOp::Deref, _) = parent.kind {
            // if the user explicitly dereferences the result, we can adjust
            // the span to also include the deref part
            span = parent.span;
        }
        false
    } else {
        true
    };

    let mut_str = if is_mut { "_mut" } else { "" };

    span_lint_and_then(
        cx,
        GET_UNWRAP,
        span,
        format!("called `.get{mut_str}().unwrap()` on a {caller_type}"),
        |diag| {
            let mut applicability = Applicability::MachineApplicable;
            let get_args_str = snippet_with_applicability(cx, get_arg.span, "..", &mut applicability);

            let borrow_str = if !needs_ref {
                ""
            } else if is_mut {
                "&mut "
            } else {
                "&"
            };

            diag.span_suggestion_verbose(
                span,
                "using `[]` is clearer and more concise",
                format!(
                    "{borrow_str}{}[{get_args_str}]",
                    snippet_with_applicability(cx, recv.span, "..", &mut applicability)
                ),
                applicability,
            );
        },
    );
}
