use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::ty_from_hir_ty;
use clippy_utils::{SpanlessEq, is_in_const_context, is_integer_literal, sym};
use rustc_errors::Applicability;
use rustc_hir::{BinOpKind, Expr, ExprKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::impl_lint_pass;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for expressions like `x.count_ones() == 1` or `x & (x - 1) == 0`, with x and unsigned integer, which may be manual
    /// reimplementations of `x.is_power_of_two()`.
    ///
    /// ### Why is this bad?
    /// Manual reimplementations of `is_power_of_two` increase code complexity for little benefit.
    ///
    /// ### Example
    /// ```no_run
    /// let a: u32 = 4;
    /// let result = a.count_ones() == 1;
    /// ```
    /// Use instead:
    /// ```no_run
    /// let a: u32 = 4;
    /// let result = a.is_power_of_two();
    /// ```
    #[clippy::version = "1.83.0"]
    pub MANUAL_IS_POWER_OF_TWO,
    pedantic,
    "manually reimplementing `is_power_of_two`"
}

pub struct ManualIsPowerOfTwo {
    msrv: Msrv,
}

impl_lint_pass!(ManualIsPowerOfTwo => [MANUAL_IS_POWER_OF_TWO]);

impl ManualIsPowerOfTwo {
    pub fn new(conf: &'static Conf) -> Self {
        Self { msrv: conf.msrv }
    }

    fn build_sugg(&self, cx: &LateContext<'_>, expr: &Expr<'_>, receiver: &Expr<'_>) {
        if is_in_const_context(cx) && !self.msrv.meets(cx, msrvs::CONST_IS_POWER_OF_TWO) {
            return;
        }

        let mut applicability = Applicability::MachineApplicable;
        let snippet = Sugg::hir_with_applicability(cx, receiver, "_", &mut applicability);

        span_lint_and_sugg(
            cx,
            MANUAL_IS_POWER_OF_TWO,
            expr.span,
            "manually reimplementing `is_power_of_two`",
            "consider using `.is_power_of_two()`",
            format!("{}.is_power_of_two()", snippet.maybe_paren()),
            applicability,
        );
    }
}

impl<'tcx> LateLintPass<'tcx> for ManualIsPowerOfTwo {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
        if !expr.span.from_expansion()
            && let Some((lhs, rhs)) = unexpanded_binop_operands(expr, BinOpKind::Eq)
        {
            if let Some(a) = count_ones_receiver(cx, lhs)
                && is_integer_literal(rhs, 1)
            {
                self.build_sugg(cx, expr, a);
            } else if let Some(a) = count_ones_receiver(cx, rhs)
                && is_integer_literal(lhs, 1)
            {
                self.build_sugg(cx, expr, a);
            } else if is_integer_literal(rhs, 0)
                && let Some(a) = is_and_minus_one(cx, lhs)
            {
                self.build_sugg(cx, expr, a);
            } else if is_integer_literal(lhs, 0)
                && let Some(a) = is_and_minus_one(cx, rhs)
            {
                self.build_sugg(cx, expr, a);
            }
        }
    }
}

/// Return the unsigned integer receiver of `.count_ones()` or the argument of
/// `<int-type>::count_ones(…)`.
fn count_ones_receiver<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
    let (method, ty, receiver) = if let ExprKind::MethodCall(method_name, receiver, [], _) = expr.kind {
        (method_name, cx.typeck_results().expr_ty_adjusted(receiver), receiver)
    } else if let ExprKind::Call(func, [arg]) = expr.kind
        && let ExprKind::Path(QPath::TypeRelative(ty, func_name)) = func.kind
    {
        (func_name, ty_from_hir_ty(cx, ty), arg)
    } else {
        return None;
    };
    (method.ident.name == sym::count_ones && matches!(ty.kind(), ty::Uint(_))).then_some(receiver)
}

/// Return `greater` if `smaller == greater - 1`
fn is_one_less<'tcx>(
    cx: &LateContext<'tcx>,
    greater: &'tcx Expr<'tcx>,
    smaller: &Expr<'tcx>,
) -> Option<&'tcx Expr<'tcx>> {
    if let Some((lhs, rhs)) = unexpanded_binop_operands(smaller, BinOpKind::Sub)
        && SpanlessEq::new(cx).eq_expr(greater, lhs)
        && is_integer_literal(rhs, 1)
        && matches!(cx.typeck_results().expr_ty_adjusted(greater).kind(), ty::Uint(_))
    {
        Some(greater)
    } else {
        None
    }
}

/// Return `v` if `expr` is `v & (v - 1)` or `(v - 1) & v`
fn is_and_minus_one<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
    let (lhs, rhs) = unexpanded_binop_operands(expr, BinOpKind::BitAnd)?;
    is_one_less(cx, lhs, rhs).or_else(|| is_one_less(cx, rhs, lhs))
}

/// Return the operands of the `expr` binary operation if the operator is `op` and none of the
/// operands come from expansion.
fn unexpanded_binop_operands<'hir>(expr: &Expr<'hir>, op: BinOpKind) -> Option<(&'hir Expr<'hir>, &'hir Expr<'hir>)> {
    if let ExprKind::Binary(binop, lhs, rhs) = expr.kind
        && binop.node == op
        && !lhs.span.from_expansion()
        && !rhs.span.from_expansion()
    {
        Some((lhs, rhs))
    } else {
        None
    }
}
