use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::res::MaybeDef;
use clippy_utils::source::{IntoSpan, SpanRangeExt};
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::ty_from_hir_ty;
use rustc_errors::{Applicability, Diag};
use rustc_hir::{self as hir, Expr, ExprKind, Item, ItemKind, LetStmt, QPath};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::mir::Mutability;
use rustc_middle::ty::{self, IntTy, Ty, UintTy};
use rustc_session::declare_lint_pass;
use rustc_span::sym;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for usage of `Mutex<X>` where an atomic will do.
    ///
    /// ### Why restrict this?
    /// Using a mutex just to make access to a plain bool or
    /// reference sequential is shooting flies with cannons.
    /// `std::sync::atomic::AtomicBool` and `std::sync::atomic::AtomicPtr` are leaner and
    /// faster.
    ///
    /// On the other hand, `Mutex`es are, in general, easier to
    /// verify correctness. An atomic does not behave the same as
    /// an equivalent mutex. See [this issue](https://github.com/rust-lang/rust-clippy/issues/4295)'s
    /// commentary for more details.
    ///
    /// ### Known problems
    /// * This lint cannot detect if the mutex is actually used
    ///   for waiting before a critical section.
    /// * This lint has a false positive that warns without considering the case
    ///   where `Mutex` is used together with `Condvar`.
    ///
    /// ### Example
    /// ```no_run
    /// # let y = true;
    /// # use std::sync::Mutex;
    /// let x = Mutex::new(&y);
    /// ```
    ///
    /// Use instead:
    /// ```no_run
    /// # let y = true;
    /// # use std::sync::atomic::AtomicBool;
    /// let x = AtomicBool::new(y);
    /// ```
    #[clippy::version = "pre 1.29.0"]
    pub MUTEX_ATOMIC,
    restriction,
    "using a mutex where an atomic value could be used instead."
}

declare_clippy_lint! {
    /// ### What it does
    /// Checks for usage of `Mutex<X>` where `X` is an integral
    /// type.
    ///
    /// ### Why restrict this?
    /// Using a mutex just to make access to a plain integer
    /// sequential is
    /// shooting flies with cannons. `std::sync::atomic::AtomicUsize` is leaner and faster.
    ///
    /// On the other hand, `Mutex`es are, in general, easier to
    /// verify correctness. An atomic does not behave the same as
    /// an equivalent mutex. See [this issue](https://github.com/rust-lang/rust-clippy/issues/4295)'s
    /// commentary for more details.
    ///
    /// ### Known problems
    /// * This lint cannot detect if the mutex is actually used
    ///   for waiting before a critical section.
    /// * This lint has a false positive that warns without considering the case
    ///   where `Mutex` is used together with `Condvar`.
    /// * This lint suggest using `AtomicU64` instead of `Mutex<u64>`, but
    ///   `AtomicU64` is not available on some 32-bit platforms.
    ///
    /// ### Example
    /// ```no_run
    /// # use std::sync::Mutex;
    /// let x = Mutex::new(0usize);
    /// ```
    ///
    /// Use instead:
    /// ```no_run
    /// # use std::sync::atomic::AtomicUsize;
    /// let x = AtomicUsize::new(0usize);
    /// ```
    #[clippy::version = "pre 1.29.0"]
    pub MUTEX_INTEGER,
    restriction,
    "using a mutex for an integer type"
}

declare_lint_pass!(Mutex => [MUTEX_ATOMIC, MUTEX_INTEGER]);

// NOTE: we don't use `check_expr` because that would make us lint every _use_ of such mutexes, not
// just their definitions
impl<'tcx> LateLintPass<'tcx> for Mutex {
    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
        if !item.span.from_expansion()
            && let ItemKind::Static(_, _, ty, body_id) = item.kind
        {
            let body = cx.tcx.hir_body(body_id);
            let mid_ty = ty_from_hir_ty(cx, ty);
            check_expr(cx, body.value.peel_blocks(), &TypeAscriptionKind::Required(ty), mid_ty);
        }
    }
    fn check_local(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx LetStmt<'_>) {
        if !stmt.span.from_expansion()
            && let Some(init) = stmt.init
        {
            let mid_ty = cx.typeck_results().expr_ty(init);
            check_expr(cx, init.peel_blocks(), &TypeAscriptionKind::Optional(stmt.ty), mid_ty);
        }
    }
}

/// Whether the type ascription `: Mutex<X>` (which we'll suggest replacing with `AtomicX`) is
/// required
enum TypeAscriptionKind<'tcx> {
    /// Yes; for us, this is the case for statics
    Required(&'tcx hir::Ty<'tcx>),
    /// No; the ascription might've been necessary in an expression like:
    /// ```ignore
    /// let mutex: Mutex<u64> = Mutex::new(0);
    /// ```
    /// to specify the type of `0`, but since `AtomicX` already refers to a concrete type, we won't
    /// need this ascription anymore.
    Optional(Option<&'tcx hir::Ty<'tcx>>),
}

fn check_expr<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>, ty_ascription: &TypeAscriptionKind<'tcx>, ty: Ty<'tcx>) {
    if let ty::Adt(_, subst) = ty.kind()
        && ty.is_diag_item(cx, sym::Mutex)
        && let mutex_param = subst.type_at(0)
        && let Some(atomic_name) = get_atomic_name(mutex_param)
    {
        let msg = "using a `Mutex` where an atomic would do";
        let diag = |diag: &mut Diag<'_, _>| {
            // if `expr = Mutex::new(arg)`, we can try emitting a suggestion
            if let ExprKind::Call(qpath, [arg]) = expr.kind
                && let ExprKind::Path(QPath::TypeRelative(_mutex, new)) = qpath.kind
                && new.ident.name == sym::new
            {
                let mut applicability = Applicability::MaybeIncorrect;
                let arg = Sugg::hir_with_context(cx, arg, expr.span.ctxt(), "_", &mut applicability);
                let mut suggs = vec![(expr.span, format!("std::sync::atomic::{atomic_name}::new({arg})"))];
                match ty_ascription {
                    TypeAscriptionKind::Required(ty_ascription) => {
                        suggs.push((ty_ascription.span, format!("std::sync::atomic::{atomic_name}")));
                    },
                    TypeAscriptionKind::Optional(Some(ty_ascription)) => {
                        // See https://github.com/rust-lang/rust-clippy/pull/15386 for why this is
                        // required
                        let colon_ascription = (cx.sess().source_map())
                            .span_extend_to_prev_char_before(ty_ascription.span, ':', true)
                            .with_leading_whitespace(cx)
                            .into_span();
                        suggs.push((colon_ascription, String::new()));
                    },
                    TypeAscriptionKind::Optional(None) => {}, // nothing to remove/replace
                }
                diag.multipart_suggestion("try", suggs, applicability);
            } else {
                diag.help(format!("consider using an `{atomic_name}` instead"));
            }
            diag.help("if you just want the locking behavior and not the internal type, consider using `Mutex<()>`");
        };
        match *mutex_param.kind() {
            ty::Uint(t) if t != UintTy::Usize => span_lint_and_then(cx, MUTEX_INTEGER, expr.span, msg, diag),
            ty::Int(t) if t != IntTy::Isize => span_lint_and_then(cx, MUTEX_INTEGER, expr.span, msg, diag),
            _ => span_lint_and_then(cx, MUTEX_ATOMIC, expr.span, msg, diag),
        }
    }
}

fn get_atomic_name(ty: Ty<'_>) -> Option<&'static str> {
    match ty.kind() {
        ty::Bool => Some("AtomicBool"),
        ty::Uint(uint_ty) => {
            match uint_ty {
                UintTy::U8 => Some("AtomicU8"),
                UintTy::U16 => Some("AtomicU16"),
                UintTy::U32 => Some("AtomicU32"),
                UintTy::U64 => Some("AtomicU64"),
                UintTy::Usize => Some("AtomicUsize"),
                // `AtomicU128` is unstable and only available on a few platforms: https://github.com/rust-lang/rust/issues/99069
                UintTy::U128 => None,
            }
        },
        ty::Int(int_ty) => {
            match int_ty {
                IntTy::I8 => Some("AtomicI8"),
                IntTy::I16 => Some("AtomicI16"),
                IntTy::I32 => Some("AtomicI32"),
                IntTy::I64 => Some("AtomicI64"),
                IntTy::Isize => Some("AtomicIsize"),
                // `AtomicU128` is unstable and only available on a few platforms: https://github.com/rust-lang/rust/issues/99069
                IntTy::I128 => None,
            }
        },
        // `AtomicPtr` only accepts `*mut T`
        ty::RawPtr(_, Mutability::Mut) => Some("AtomicPtr"),
        _ => None,
    }
}
