use super::utils::clone_or_copy_needed;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::higher::ForLoop;
use clippy_utils::source::snippet_opt;
use clippy_utils::ty::{get_associated_type, get_iterator_item_ty, implements_trait};
use clippy_utils::{fn_def_id, get_parent_expr};
use rustc_errors::Applicability;
use rustc_hir::{def_id::DefId, Expr, ExprKind, LangItem};
use rustc_lint::LateContext;
use rustc_span::{sym, Symbol};

use super::UNNECESSARY_TO_OWNED;

pub fn check(cx: &LateContext<'_>, expr: &Expr<'_>, method_name: Symbol, receiver: &Expr<'_>) -> bool {
    if_chain! {
        if let Some(parent) = get_parent_expr(cx, expr);
        if let Some(callee_def_id) = fn_def_id(cx, parent);
        if is_into_iter(cx, callee_def_id);
        then {
            check_for_loop_iter(cx, parent, method_name, receiver, false)
        } else {
            false
        }
    }
}

/// Checks whether `expr` is an iterator in a `for` loop and, if so, determines whether the
/// iterated-over items could be iterated over by reference. The reason why `check` above does not
/// include this code directly is so that it can be called from
/// `unnecessary_into_owned::check_into_iter_call_arg`.
pub fn check_for_loop_iter(
    cx: &LateContext<'_>,
    expr: &Expr<'_>,
    method_name: Symbol,
    receiver: &Expr<'_>,
    cloned_before_iter: bool,
) -> bool {
    if_chain! {
        if let Some(grandparent) = get_parent_expr(cx, expr).and_then(|parent| get_parent_expr(cx, parent));
        if let Some(ForLoop { pat, body, .. }) = ForLoop::hir(grandparent);
        let (clone_or_copy_needed, addr_of_exprs) = clone_or_copy_needed(cx, pat, body);
        if !clone_or_copy_needed;
        if let Some(receiver_snippet) = snippet_opt(cx, receiver.span);
        then {
            let snippet = if_chain! {
                if let ExprKind::MethodCall(maybe_iter_method_name, [collection], _) = receiver.kind;
                if maybe_iter_method_name.ident.name == sym::iter;

                if let Some(iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::Iterator);
                let receiver_ty = cx.typeck_results().expr_ty(receiver);
                if implements_trait(cx, receiver_ty, iterator_trait_id, &[]);
                if let Some(iter_item_ty) = get_iterator_item_ty(cx, receiver_ty);

                if let Some(into_iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::IntoIterator);
                let collection_ty = cx.typeck_results().expr_ty(collection);
                if implements_trait(cx, collection_ty, into_iterator_trait_id, &[]);
                if let Some(into_iter_item_ty) = get_associated_type(cx, collection_ty, into_iterator_trait_id, "Item");

                if iter_item_ty == into_iter_item_ty;
                if let Some(collection_snippet) = snippet_opt(cx, collection.span);
                then {
                    collection_snippet
                } else {
                    receiver_snippet
                }
            };
            span_lint_and_then(
                cx,
                UNNECESSARY_TO_OWNED,
                expr.span,
                &format!("unnecessary use of `{}`", method_name),
                |diag| {
                    // If `check_into_iter_call_arg` called `check_for_loop_iter` because a call to
                    // a `to_owned`-like function was removed, then the next suggestion may be
                    // incorrect. This is because the iterator that results from the call's removal
                    // could hold a reference to a resource that is used mutably. See
                    // https://github.com/rust-lang/rust-clippy/issues/8148.
                    let applicability = if cloned_before_iter {
                        Applicability::MaybeIncorrect
                    } else {
                        Applicability::MachineApplicable
                    };
                    diag.span_suggestion(expr.span, "use", snippet, applicability);
                    for addr_of_expr in addr_of_exprs {
                        match addr_of_expr.kind {
                            ExprKind::AddrOf(_, _, referent) => {
                                let span = addr_of_expr.span.with_hi(referent.span.lo());
                                diag.span_suggestion(span, "remove this `&`", String::new(), applicability);
                            }
                            _ => unreachable!(),
                        }
                    }
                }
            );
            return true;
        }
    }
    false
}

/// Returns true if the named method is `IntoIterator::into_iter`.
pub fn is_into_iter(cx: &LateContext<'_>, callee_def_id: DefId) -> bool {
    cx.tcx.lang_items().require(LangItem::IntoIterIntoIter) == Ok(callee_def_id)
}
