use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
use clippy_utils::source::{SpanRangeExt as _, snippet_with_applicability};
use clippy_utils::{SpanlessEq, get_parent_expr, higher, is_integer_const, sym};
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, Node, Pat, PatKind, QPath};
use rustc_lint::LateContext;

use super::RANGE_ZIP_WITH_LEN;

pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, recv: &'tcx Expr<'_>, zip_arg: &'tcx Expr<'_>) {
    if cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator)
        // range expression in `.zip()` call: `0..x.len()`
        && let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::Range::hir(cx, zip_arg)
        && is_integer_const(cx, start, 0)
        // `.len()` call
        && let ExprKind::MethodCall(len_path, len_recv, [], _) = end.kind
        && len_path.ident.name == sym::len
        // `.iter()` and `.len()` called on same `Path`
        && let ExprKind::Path(QPath::Resolved(_, iter_path)) = recv.kind
        && let ExprKind::Path(QPath::Resolved(_, len_path)) = len_recv.kind
        && SpanlessEq::new(cx).eq_path_segments(iter_path.segments, len_path.segments)
    {
        span_lint_and_then(
            cx,
            RANGE_ZIP_WITH_LEN,
            expr.span,
            "using `.zip()` with a range and `.len()`",
            |diag| {
                // If the iterator content is consumed by a pattern with exactly two elements, swap
                // the order of those elements. Otherwise, the suggestion will be marked as
                // `Applicability::MaybeIncorrect` (because it will be), and a note will be added
                // to the diagnostic to underline the swapping of the index and the content.
                let pat = methods_pattern(cx, expr).or_else(|| for_loop_pattern(cx, expr));
                let invert_bindings = if let Some(pat) = pat
                    && pat.span.eq_ctxt(expr.span)
                    && let PatKind::Tuple([first, second], _) = pat.kind
                {
                    Some((first.span, second.span))
                } else {
                    None
                };
                let mut app = Applicability::MachineApplicable;
                let mut suggestions = vec![(
                    expr.span,
                    format!(
                        "{}.iter().enumerate()",
                        snippet_with_applicability(cx, recv.span, "_", &mut app)
                    ),
                )];
                if let Some((left, right)) = invert_bindings
                    && let Some(snip_left) = left.get_source_text(cx)
                    && let Some(snip_right) = right.get_source_text(cx)
                {
                    suggestions.extend([(left, snip_right.to_string()), (right, snip_left.to_string())]);
                } else {
                    app = Applicability::MaybeIncorrect;
                }
                diag.multipart_suggestion("use", suggestions, app);
                if app != Applicability::MachineApplicable {
                    diag.note("the order of the element and the index will be swapped");
                }
            },
        );
    }
}

/// If `expr` is the argument of a `for` loop, return the loop pattern.
fn for_loop_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Pat<'tcx>> {
    cx.tcx.hir_parent_iter(expr.hir_id).find_map(|(_, node)| {
        if let Node::Expr(ancestor_expr) = node
            && let Some(for_loop) = higher::ForLoop::hir(ancestor_expr)
            && for_loop.arg.hir_id == expr.hir_id
        {
            Some(for_loop.pat)
        } else {
            None
        }
    })
}

/// If `expr` is the receiver of an `Iterator` method which consumes the iterator elements and feed
/// them to a closure, return the pattern of the closure.
fn methods_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Pat<'tcx>> {
    if let Some(parent_expr) = get_parent_expr(cx, expr)
        && cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator)
        && let ExprKind::MethodCall(method, recv, [arg], _) = parent_expr.kind
        && recv.hir_id == expr.hir_id
        && matches!(
            method.ident.name,
            sym::all
                | sym::any
                | sym::filter_map
                | sym::find_map
                | sym::flat_map
                | sym::for_each
                | sym::is_partitioned
                | sym::is_sorted_by_key
                | sym::map
                | sym::map_while
                | sym::position
                | sym::rposition
                | sym::try_for_each
        )
        && let ExprKind::Closure(closure) = arg.kind
        && let body = cx.tcx.hir_body(closure.body)
        && let [param] = body.params
    {
        Some(param.pat)
    } else {
        None
    }
}
