use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::Msrv;
use clippy_utils::qualify_min_const_fn::is_stable_const_fn;
use clippy_utils::source::SpanRangeExt;
use clippy_utils::ty::implements_trait;
use clippy_utils::visitors::for_each_expr_without_closures;
use clippy_utils::{binop_traits, eq_expr_value, is_in_const_context, trait_ref_of_method};
use core::ops::ControlFlow;
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_hir::{HirId, HirIdSet};
use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId};
use rustc_lint::LateContext;
use rustc_middle::mir::FakeReadCause;
use rustc_middle::ty::BorrowKind;

use super::ASSIGN_OP_PATTERN;

pub(super) fn check<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &'tcx hir::Expr<'_>,
    assignee: &'tcx hir::Expr<'_>,
    e: &'tcx hir::Expr<'_>,
    msrv: Msrv,
) {
    if let hir::ExprKind::Binary(op, l, r) = &e.kind {
        let lint = |assignee: &hir::Expr<'_>, rhs: &hir::Expr<'_>| {
            let ty = cx.typeck_results().expr_ty(assignee);
            let rty = cx.typeck_results().expr_ty(rhs);
            if let Some((_, lang_item)) = binop_traits(op.node)
                && let Some(trait_id) = cx.tcx.lang_items().get(lang_item)
                && let parent_fn = cx.tcx.hir_get_parent_item(e.hir_id)
                && trait_ref_of_method(cx, parent_fn).is_none_or(|t| t.path.res.def_id() != trait_id)
                && implements_trait(cx, ty, trait_id, &[rty.into()])
            {
                // Primitive types execute assign-ops right-to-left. Every other type is left-to-right.
                if !(ty.is_primitive() && rty.is_primitive()) {
                    // TODO: This will have false negatives as it doesn't check if the borrows are
                    // actually live at the end of their respective expressions.
                    let mut_borrows = mut_borrows_in_expr(cx, assignee);
                    let imm_borrows = imm_borrows_in_expr(cx, rhs);
                    if mut_borrows.iter().any(|id| imm_borrows.contains(id)) {
                        return;
                    }
                }

                // Skip if the trait or the implementation is not stable in const contexts
                if is_in_const_context(cx) {
                    if cx
                        .tcx
                        .associated_item_def_ids(trait_id)
                        .first()
                        .is_none_or(|binop_id| !is_stable_const_fn(cx, *binop_id, msrv))
                    {
                        return;
                    }

                    let impls = cx.tcx.non_blanket_impls_for_ty(trait_id, rty).collect::<Vec<_>>();
                    if impls.is_empty()
                        || impls.into_iter().any(|impl_id| {
                            cx.tcx
                                .associated_item_def_ids(impl_id)
                                .first()
                                .is_none_or(|fn_id| !is_stable_const_fn(cx, *fn_id, msrv))
                        })
                    {
                        return;
                    }
                }

                span_lint_and_then(
                    cx,
                    ASSIGN_OP_PATTERN,
                    expr.span,
                    "manual implementation of an assign operation",
                    |diag| {
                        if let Some(snip_a) = assignee.span.get_source_text(cx)
                            && let Some(snip_r) = rhs.span.get_source_text(cx)
                        {
                            diag.span_suggestion(
                                expr.span,
                                "replace it with",
                                format!("{snip_a} {}= {snip_r}", op.node.as_str()),
                                Applicability::MachineApplicable,
                            );
                        }
                    },
                );
            }
        };

        let mut found = false;
        let found_multiple = for_each_expr_without_closures(e, |e| {
            if eq_expr_value(cx, assignee, e) {
                if found {
                    return ControlFlow::Break(());
                }
                found = true;
            }
            ControlFlow::Continue(())
        })
        .is_some();

        if found && !found_multiple {
            // a = a op b
            if eq_expr_value(cx, assignee, l) {
                lint(assignee, r);
            }
            // a = b commutative_op a
            // Limited to primitive type as these ops are know to be commutative
            if eq_expr_value(cx, assignee, r) && cx.typeck_results().expr_ty(assignee).is_primitive_ty() {
                match op.node {
                    hir::BinOpKind::Add
                    | hir::BinOpKind::Mul
                    | hir::BinOpKind::And
                    | hir::BinOpKind::Or
                    | hir::BinOpKind::BitXor
                    | hir::BinOpKind::BitAnd
                    | hir::BinOpKind::BitOr => {
                        lint(assignee, l);
                    },
                    _ => {},
                }
            }
        }
    }
}

fn imm_borrows_in_expr(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> HirIdSet {
    struct S(HirIdSet);
    impl Delegate<'_> for S {
        fn borrow(&mut self, place: &PlaceWithHirId<'_>, _: HirId, kind: BorrowKind) {
            if matches!(kind, BorrowKind::Immutable | BorrowKind::UniqueImmutable) {
                self.0.insert(match place.place.base {
                    PlaceBase::Local(id) => id,
                    PlaceBase::Upvar(id) => id.var_path.hir_id,
                    _ => return,
                });
            }
        }

        fn consume(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
        fn use_cloned(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
        fn mutate(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
        fn fake_read(&mut self, _: &PlaceWithHirId<'_>, _: FakeReadCause, _: HirId) {}
        fn copy(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
    }

    let mut s = S(HirIdSet::default());
    let v = ExprUseVisitor::for_clippy(cx, e.hir_id.owner.def_id, &mut s);
    v.consume_expr(e).into_ok();
    s.0
}

fn mut_borrows_in_expr(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> HirIdSet {
    struct S(HirIdSet);
    impl Delegate<'_> for S {
        fn borrow(&mut self, place: &PlaceWithHirId<'_>, _: HirId, kind: BorrowKind) {
            if matches!(kind, BorrowKind::Mutable) {
                self.0.insert(match place.place.base {
                    PlaceBase::Local(id) => id,
                    PlaceBase::Upvar(id) => id.var_path.hir_id,
                    _ => return,
                });
            }
        }

        fn consume(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
        fn use_cloned(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
        fn mutate(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
        fn fake_read(&mut self, _: &PlaceWithHirId<'_>, _: FakeReadCause, _: HirId) {}
        fn copy(&mut self, _: &PlaceWithHirId<'_>, _: HirId) {}
    }

    let mut s = S(HirIdSet::default());
    let v = ExprUseVisitor::for_clippy(cx, e.hir_id.owner.def_id, &mut s);
    v.consume_expr(e).into_ok();
    s.0
}
