use std::ops::ControlFlow;

use super::WHILE_LET_ON_ITERATOR;
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::visitors::is_res_used;
use clippy_utils::{as_some_pattern, get_enclosing_loop_or_multi_call_closure, higher, is_refutable};
use rustc_errors::Applicability;
use rustc_hir::def::Res;
use rustc_hir::intravisit::{Visitor, walk_expr};
use rustc_hir::{Closure, Expr, ExprKind, HirId, LetStmt, Mutability, UnOp};
use rustc_lint::LateContext;
use rustc_middle::hir::nested_filter::OnlyBodies;
use rustc_middle::ty::adjustment::Adjust;
use rustc_span::Symbol;
use rustc_span::symbol::sym;

pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
    if let Some(higher::WhileLet { if_then, let_pat, let_expr, label, .. }) = higher::WhileLet::hir(expr)
        // check for `Some(..)` pattern
        && let Some(some_pat) = as_some_pattern(cx, let_pat)
        // check for call to `Iterator::next`
        && let ExprKind::MethodCall(method_name, iter_expr, [], _) = let_expr.kind
        && method_name.ident.name == sym::next
        && cx.ty_based_def(let_expr).opt_parent(cx).is_diag_item(cx, sym::Iterator)
        && let Some(iter_expr_struct) = try_parse_iter_expr(cx, iter_expr)
        // get the loop containing the match expression
        && !uses_iter(cx, &iter_expr_struct, if_then)
    {
        let mut applicability = Applicability::MachineApplicable;

        let loop_label = label.map_or(String::new(), |l| format!("{}: ", l.ident.name));

        let loop_var = if let Some(some_pat) = some_pat.first() {
            if is_refutable(cx, some_pat) {
                // Refutable patterns don't work with for loops.
                return;
            }
            snippet_with_applicability(cx, some_pat.span, "..", &mut applicability)
        } else {
            "_".into()
        };

        // If the iterator is a field or the iterator is accessed after the loop is complete it needs to be
        // passed by reference. TODO: If the struct can be partially moved from and the struct isn't used
        // afterwards a mutable borrow of a field isn't necessary.
        let iterator = snippet_with_applicability(cx, iter_expr.span, "_", &mut applicability);
        let iterator_by_ref = if cx.typeck_results().expr_ty(iter_expr).ref_mutability() == Some(Mutability::Mut)
            || !iter_expr_struct.can_move
            || !iter_expr_struct.fields.is_empty()
            || needs_mutable_borrow(cx, &iter_expr_struct, expr)
        {
            make_iterator_snippet(cx, iter_expr, &iterator)
        } else {
            iterator.into_owned()
        };

        span_lint_and_sugg(
            cx,
            WHILE_LET_ON_ITERATOR,
            expr.span.with_hi(let_expr.span.hi()),
            "this loop could be written as a `for` loop",
            "try",
            format!("{loop_label}for {loop_var} in {iterator_by_ref}"),
            applicability,
        );
    }
}

#[derive(Debug)]
struct IterExpr {
    /// The fields used, in order of child to parent.
    fields: Vec<Symbol>,
    /// The path being used.
    path: Res,
    /// Whether or not the iterator can be moved.
    can_move: bool,
}

/// Parses any expression to find out which field of which variable is used. Will return `None` if
/// the expression might have side effects.
fn try_parse_iter_expr(cx: &LateContext<'_>, mut e: &Expr<'_>) -> Option<IterExpr> {
    let mut fields = Vec::new();
    let mut can_move = true;
    loop {
        if cx
            .typeck_results()
            .expr_adjustments(e)
            .iter()
            .any(|a| matches!(a.kind, Adjust::Deref(Some(..))))
        {
            // Custom deref impls need to borrow the whole value as it's captured by reference
            can_move = false;
            fields.clear();
        }
        match e.kind {
            ExprKind::Path(ref path) => {
                break Some(IterExpr {
                    fields,
                    path: cx.qpath_res(path, e.hir_id),
                    can_move,
                });
            },
            ExprKind::Field(base, name) => {
                fields.push(name.name);
                e = base;
            },
            // Dereferencing a pointer has no side effects and doesn't affect which field is being used.
            ExprKind::Unary(UnOp::Deref, base) if cx.typeck_results().expr_ty(base).is_ref() => e = base,

            // Shouldn't have side effects, but there's no way to trace which field is used. So forget which fields have
            // already been seen.
            ExprKind::Index(base, idx, _) if !idx.can_have_side_effects() => {
                can_move = false;
                fields.clear();
                e = base;
            },
            ExprKind::Unary(UnOp::Deref, base) => {
                can_move = false;
                fields.clear();
                e = base;
            },

            // No effect and doesn't affect which field is being used.
            ExprKind::DropTemps(base) | ExprKind::AddrOf(_, _, base) | ExprKind::Type(base, _) => e = base,
            _ => break None,
        }
    }
}

fn is_expr_same_field(cx: &LateContext<'_>, mut e: &Expr<'_>, mut fields: &[Symbol], path_res: Res) -> bool {
    loop {
        match (&e.kind, fields) {
            (&ExprKind::Field(base, name), [head_field, tail_fields @ ..]) if name.name == *head_field => {
                e = base;
                fields = tail_fields;
            },
            (ExprKind::Path(path), []) => {
                break cx.qpath_res(path, e.hir_id) == path_res;
            },
            (&(ExprKind::DropTemps(base) | ExprKind::AddrOf(_, _, base) | ExprKind::Type(base, _)), _) => e = base,
            _ => break false,
        }
    }
}

/// Checks if the given expression is the same field as, is a child of, or is the parent of the
/// given field. Used to check if the expression can be used while the given field is borrowed
/// mutably. e.g. if checking for `x.y`, then `x.y`, `x.y.z`, and `x` will all return true, but
/// `x.z`, and `y` will return false.
fn is_expr_same_child_or_parent_field(cx: &LateContext<'_>, expr: &Expr<'_>, fields: &[Symbol], path_res: Res) -> bool {
    match expr.kind {
        ExprKind::Field(base, name) => {
            if let Some((head_field, tail_fields)) = fields.split_first() {
                if name.name == *head_field && is_expr_same_field(cx, base, tail_fields, path_res) {
                    return true;
                }
                // Check if the expression is a parent field
                let mut fields_iter = tail_fields.iter();
                while let Some(field) = fields_iter.next() {
                    if *field == name.name && is_expr_same_field(cx, base, fields_iter.as_slice(), path_res) {
                        return true;
                    }
                }
            }

            // Check if the expression is a child field.
            let mut e = base;
            loop {
                match e.kind {
                    ExprKind::Field(..) if is_expr_same_field(cx, e, fields, path_res) => break true,
                    ExprKind::Field(base, _) | ExprKind::DropTemps(base) | ExprKind::Type(base, _) => e = base,
                    ExprKind::Path(ref path) if fields.is_empty() => {
                        break cx.qpath_res(path, e.hir_id) == path_res;
                    },
                    _ => break false,
                }
            }
        },
        // If the path matches, this is either an exact match, or the expression is a parent of the field.
        ExprKind::Path(ref path) => cx.qpath_res(path, expr.hir_id) == path_res,
        ExprKind::DropTemps(base) | ExprKind::Type(base, _) | ExprKind::AddrOf(_, _, base) => {
            is_expr_same_child_or_parent_field(cx, base, fields, path_res)
        },
        _ => false,
    }
}

/// Strips off all field and path expressions. This will return true if a field or path has been
/// skipped. Used to skip them after failing to check for equality.
fn skip_fields_and_path<'tcx>(expr: &'tcx Expr<'_>) -> (Option<&'tcx Expr<'tcx>>, bool) {
    let mut e = expr;
    let e = loop {
        match e.kind {
            ExprKind::Field(base, _) | ExprKind::DropTemps(base) | ExprKind::Type(base, _) => e = base,
            ExprKind::Path(_) => return (None, true),
            _ => break e,
        }
    };
    (Some(e), e.hir_id != expr.hir_id)
}

/// Checks if the given expression uses the iterator.
fn uses_iter<'tcx>(cx: &LateContext<'tcx>, iter_expr: &IterExpr, container: &'tcx Expr<'_>) -> bool {
    struct V<'a, 'b, 'tcx> {
        cx: &'a LateContext<'tcx>,
        iter_expr: &'b IterExpr,
    }
    impl<'tcx> Visitor<'tcx> for V<'_, '_, 'tcx> {
        type Result = ControlFlow<()>;
        fn visit_expr(&mut self, e: &'tcx Expr<'_>) -> Self::Result {
            if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) {
                ControlFlow::Break(())
            } else if let (e, true) = skip_fields_and_path(e) {
                if let Some(e) = e {
                    self.visit_expr(e)
                } else {
                    ControlFlow::Continue(())
                }
            } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind {
                if is_res_used(self.cx, self.iter_expr.path, id) {
                    ControlFlow::Break(())
                } else {
                    ControlFlow::Continue(())
                }
            } else {
                walk_expr(self, e)
            }
        }
    }

    let mut v = V { cx, iter_expr };
    v.visit_expr(container).is_break()
}

#[expect(clippy::too_many_lines)]
fn needs_mutable_borrow(cx: &LateContext<'_>, iter_expr: &IterExpr, loop_expr: &Expr<'_>) -> bool {
    struct AfterLoopVisitor<'a, 'b, 'tcx> {
        cx: &'a LateContext<'tcx>,
        iter_expr: &'b IterExpr,
        loop_id: HirId,
        after_loop: bool,
    }
    impl<'tcx> Visitor<'tcx> for AfterLoopVisitor<'_, '_, 'tcx> {
        type NestedFilter = OnlyBodies;
        type Result = ControlFlow<()>;
        fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
            self.cx.tcx
        }

        fn visit_expr(&mut self, e: &'tcx Expr<'_>) -> Self::Result {
            if self.after_loop {
                if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) {
                    ControlFlow::Break(())
                } else if let (e, true) = skip_fields_and_path(e) {
                    if let Some(e) = e {
                        self.visit_expr(e)
                    } else {
                        ControlFlow::Continue(())
                    }
                } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind {
                    if is_res_used(self.cx, self.iter_expr.path, id) {
                        ControlFlow::Break(())
                    } else {
                        ControlFlow::Continue(())
                    }
                } else {
                    walk_expr(self, e)
                }
            } else if self.loop_id == e.hir_id {
                self.after_loop = true;
                ControlFlow::Continue(())
            } else {
                walk_expr(self, e)
            }
        }
    }

    struct NestedLoopVisitor<'a, 'b, 'tcx> {
        cx: &'a LateContext<'tcx>,
        iter_expr: &'b IterExpr,
        local_id: HirId,
        loop_id: HirId,
        after_loop: bool,
        found_local: bool,
        used_after: bool,
    }
    impl<'tcx> Visitor<'tcx> for NestedLoopVisitor<'_, '_, 'tcx> {
        type NestedFilter = OnlyBodies;
        fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
            self.cx.tcx
        }

        fn visit_local(&mut self, l: &'tcx LetStmt<'_>) {
            if !self.after_loop {
                l.pat.each_binding_or_first(&mut |_, id, _, _| {
                    if id == self.local_id {
                        self.found_local = true;
                    }
                });
            }
            if let Some(e) = l.init {
                self.visit_expr(e);
            }
        }

        fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
            if self.used_after {
                return;
            }
            if self.after_loop {
                if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) {
                    self.used_after = true;
                } else if let (e, true) = skip_fields_and_path(e) {
                    if let Some(e) = e {
                        self.visit_expr(e);
                    }
                } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind {
                    self.used_after = is_res_used(self.cx, self.iter_expr.path, id);
                } else {
                    walk_expr(self, e);
                }
            } else if e.hir_id == self.loop_id {
                self.after_loop = true;
            } else {
                walk_expr(self, e);
            }
        }
    }

    if let Some(e) = get_enclosing_loop_or_multi_call_closure(cx, loop_expr) {
        let Res::Local(local_id) = iter_expr.path else {
            return true;
        };
        let mut v = NestedLoopVisitor {
            cx,
            iter_expr,
            local_id,
            loop_id: loop_expr.hir_id,
            after_loop: false,
            found_local: false,
            used_after: false,
        };
        v.visit_expr(e);
        v.used_after || !v.found_local
    } else {
        let mut v = AfterLoopVisitor {
            cx,
            iter_expr,
            loop_id: loop_expr.hir_id,
            after_loop: false,
        };
        v.visit_expr(cx.tcx.hir_body(cx.enclosing_body.unwrap()).value)
            .is_break()
    }
}

/// Constructs the transformed iterator expression for the suggestion.
/// Returns `iterator.by_ref()` unless the last deref adjustment targets an unsized type,
/// in which case it applies all derefs (e.g., `&mut **iterator` or `&mut ***iterator`).
fn make_iterator_snippet<'tcx>(cx: &LateContext<'tcx>, iter_expr: &Expr<'tcx>, iterator: &str) -> String {
    if let Some((n, adjust)) = cx
        .typeck_results()
        .expr_adjustments(iter_expr)
        .iter()
        .take_while(|x| matches!(x.kind, Adjust::Deref(_)))
        .enumerate()
        .last()
        && !adjust.target.is_sized(cx.tcx, cx.typing_env())
    {
        format!("&mut {:*<n$}{iterator}", '*', n = n + 1)
    } else {
        format!("{iterator}.by_ref()")
    }
}
