use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::{get_parent_expr, usage};
use rustc_hir::intravisit::{Visitor, walk_expr};
use rustc_hir::{BorrowKind, Expr, ExprKind, HirId, Mutability, Pat, QPath, Stmt, StmtKind};
use rustc_lint::LateContext;
use rustc_middle::hir::nested_filter;
use rustc_middle::ty::{self, Ty};
use rustc_span::Span;
use rustc_span::symbol::sym;

/// Checks if `expr`, of type `ty`, corresponds to a slice or can be dereferenced to a slice, or if
/// `expr` is a method call to `.iter()` on such a type. In these cases, return the slice-like
/// expression.
pub(super) fn derefs_to_slice<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &'tcx Expr<'tcx>,
    ty: Ty<'tcx>,
) -> Option<&'tcx Expr<'tcx>> {
    fn may_slice<'a>(cx: &LateContext<'a>, ty: Ty<'a>) -> bool {
        match ty.kind() {
            ty::Slice(_) => true,
            ty::Adt(..) if let Some(boxed) = ty.boxed_ty() => may_slice(cx, boxed),
            ty::Adt(..) => ty.is_diag_item(cx, sym::Vec),
            ty::Array(_, size) => size.try_to_target_usize(cx.tcx).is_some(),
            ty::Ref(_, inner, _) => may_slice(cx, *inner),
            _ => false,
        }
    }

    if let ExprKind::MethodCall(path, self_arg, ..) = &expr.kind {
        if path.ident.name == sym::iter && may_slice(cx, cx.typeck_results().expr_ty(self_arg)) {
            Some(self_arg)
        } else {
            None
        }
    } else {
        match ty.kind() {
            ty::Slice(_) => Some(expr),
            _ if ty.boxed_ty().is_some_and(|boxed| may_slice(cx, boxed)) => Some(expr),
            ty::Ref(_, inner, _) => {
                if may_slice(cx, *inner) {
                    Some(expr)
                } else {
                    None
                }
            },
            _ => None,
        }
    }
}

/// The core logic of `check_for_loop_iter` in `unnecessary_iter_cloned.rs`, this function wraps a
/// use of `CloneOrCopyVisitor`.
pub(super) fn clone_or_copy_needed<'tcx>(
    cx: &LateContext<'tcx>,
    pat: &Pat<'tcx>,
    body: &'tcx Expr<'tcx>,
) -> (bool, Vec<(Span, String)>) {
    let mut visitor = CloneOrCopyVisitor {
        cx,
        binding_hir_ids: pat_bindings(pat),
        clone_or_copy_needed: false,
        references_to_binding: Vec::new(),
    };
    visitor.visit_expr(body);
    (visitor.clone_or_copy_needed, visitor.references_to_binding)
}

/// Returns a vector of all `HirId`s bound by the pattern.
fn pat_bindings(pat: &Pat<'_>) -> Vec<HirId> {
    let mut collector = usage::ParamBindingIdCollector {
        binding_hir_ids: Vec::new(),
    };
    collector.visit_pat(pat);
    collector.binding_hir_ids
}

/// `clone_or_copy_needed` will be false when `CloneOrCopyVisitor` is done visiting if the only
/// operations performed on `binding_hir_ids` are:
/// * to take non-mutable references to them
/// * to use them as non-mutable `&self` in method calls
///
/// If any of `binding_hir_ids` is used in any other way, then `clone_or_copy_needed` will be true
/// when `CloneOrCopyVisitor` is done visiting.
struct CloneOrCopyVisitor<'cx, 'tcx> {
    cx: &'cx LateContext<'tcx>,
    binding_hir_ids: Vec<HirId>,
    clone_or_copy_needed: bool,
    references_to_binding: Vec<(Span, String)>,
}

impl<'tcx> Visitor<'tcx> for CloneOrCopyVisitor<'_, 'tcx> {
    type NestedFilter = nested_filter::OnlyBodies;

    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
        self.cx.tcx
    }

    fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
        walk_expr(self, expr);
        if self.is_binding(expr) {
            if let Some(parent) = get_parent_expr(self.cx, expr) {
                match parent.kind {
                    ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, referent) => {
                        if !parent.span.from_expansion() {
                            self.references_to_binding
                                .push((parent.span.until(referent.span), String::new()));
                        }
                        return;
                    },
                    ExprKind::MethodCall(.., args, _) => {
                        if args.iter().all(|arg| !self.is_binding(arg))
                            && let Some(method_def_id) = self.cx.typeck_results().type_dependent_def_id(parent.hir_id)
                            && let method_ty = self.cx.tcx.type_of(method_def_id).instantiate_identity()
                            && let self_ty = method_ty.fn_sig(self.cx.tcx).input(0).skip_binder()
                            && matches!(self_ty.kind(), ty::Ref(_, _, Mutability::Not))
                        {
                            return;
                        }
                    },
                    _ => {},
                }
            }
            self.clone_or_copy_needed = true;
        }
    }
}

impl<'tcx> CloneOrCopyVisitor<'_, 'tcx> {
    fn is_binding(&self, expr: &Expr<'tcx>) -> bool {
        self.binding_hir_ids
            .iter()
            .any(|&hir_id| expr.res_local_id() == Some(hir_id))
    }
}

pub(super) fn get_last_chain_binding_hir_id(mut hir_id: HirId, statements: &[Stmt<'_>]) -> Option<HirId> {
    for stmt in statements {
        if let StmtKind::Let(local) = stmt.kind
            && let Some(init) = local.init
            && let ExprKind::Path(QPath::Resolved(_, path)) = init.kind
            && let rustc_hir::def::Res::Local(local_hir_id) = path.res
            && local_hir_id == hir_id
        {
            hir_id = local.pat.hir_id;
        } else {
            return None;
        }
    }
    Some(hir_id)
}
