use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint, span_lint_hir_and_then};
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::paths::{self, PathNS, find_crates, lookup_path_str};
use clippy_utils::res::MaybeResPath;
use clippy_utils::visitors::for_each_expr;
use clippy_utils::{fn_def_id, is_no_std_crate, sym};
use rustc_data_structures::fx::FxIndexMap;
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::{CrateNum, DefId};
use rustc_hir::{self as hir, BodyId, Expr, ExprKind, HirId, Item, ItemKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::impl_lint_pass;
use rustc_span::Span;

declare_clippy_lint! {
    /// ### What it does
    /// Lints when `once_cell::sync::Lazy` or `lazy_static!` are used to define a static variable,
    /// and suggests replacing such cases with `std::sync::LazyLock` instead.
    ///
    /// Note: This lint will not trigger in crate with `no_std` context, or with MSRV < 1.80.0. It
    /// also will not trigger on `once_cell::sync::Lazy` usage in crates which use other types
    /// from `once_cell`, such as `once_cell::race::OnceBox`.
    ///
    /// ### Why restrict this?
    /// - Reduces the need for an extra dependency
    /// - Enforce convention of using standard library types when possible
    ///
    /// ### Example
    /// ```ignore
    /// lazy_static! {
    ///     static ref FOO: String = "foo".to_uppercase();
    /// }
    /// static BAR: once_cell::sync::Lazy<String> = once_cell::sync::Lazy::new(|| "BAR".to_lowercase());
    /// ```
    /// Use instead:
    /// ```ignore
    /// static FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "FOO".to_lowercase());
    /// static BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "BAR".to_lowercase());
    /// ```
    #[clippy::version = "1.86.0"]
    pub NON_STD_LAZY_STATICS,
    pedantic,
    "lazy static that could be replaced by `std::sync::LazyLock`"
}

/// A list containing functions with corresponding replacements in `LazyLock`.
///
/// Some functions could be replaced as well if we have replaced `Lazy` to `LazyLock`,
/// therefore after suggesting replace the type, we need to make sure the function calls can be
/// replaced, otherwise the suggestions cannot be applied thus the applicability should be
/// [`Applicability::Unspecified`] or [`Applicability::MaybeIncorrect`].
static FUNCTION_REPLACEMENTS: &[(&str, Option<&str>)] = &[
    ("once_cell::sync::Lazy::force", Some("std::sync::LazyLock::force")),
    ("once_cell::sync::Lazy::get", None), // `std::sync::LazyLock::get` is experimental
    ("once_cell::sync::Lazy::new", Some("std::sync::LazyLock::new")),
    // Note: `Lazy::{into_value, get_mut, force_mut}` are not in the list.
    // Because the lint only checks for `static`s, and using these functions with statics
    // will either be a hard error or triggers `static_mut_ref` that will be hard errors.
    // But keep in mind that if somehow we decide to expand this lint to catch non-statics,
    // add those functions into the list.
];

pub struct NonStdLazyStatic {
    msrv: Msrv,
    once_cell_crates: Vec<CrateNum>,
    sugg_map: FxIndexMap<DefId, Option<String>>,
    lazy_type_defs: FxIndexMap<DefId, LazyInfo>,
    uses_other_once_cell_types: bool,
}

impl NonStdLazyStatic {
    #[must_use]
    pub fn new(conf: &'static Conf) -> Self {
        Self {
            msrv: conf.msrv,
            once_cell_crates: Vec::new(),
            sugg_map: FxIndexMap::default(),
            lazy_type_defs: FxIndexMap::default(),
            uses_other_once_cell_types: false,
        }
    }
}

impl_lint_pass!(NonStdLazyStatic => [NON_STD_LAZY_STATICS]);

fn can_use_lazy_cell(cx: &LateContext<'_>, msrv: Msrv) -> bool {
    msrv.meets(cx, msrvs::LAZY_CELL) && !is_no_std_crate(cx)
}

impl<'hir> LateLintPass<'hir> for NonStdLazyStatic {
    fn check_crate(&mut self, cx: &LateContext<'hir>) {
        // Add CrateNums for `once_cell` crate
        self.once_cell_crates = find_crates(cx.tcx, sym::once_cell)
            .iter()
            .map(|def_id| def_id.krate)
            .collect();

        // Convert hardcoded fn replacement list into a map with def_id
        for (path, sugg) in FUNCTION_REPLACEMENTS {
            for did in lookup_path_str(cx.tcx, PathNS::Value, path) {
                self.sugg_map.insert(did, sugg.map(ToOwned::to_owned));
            }
        }
    }

    fn check_item(&mut self, cx: &LateContext<'hir>, item: &Item<'hir>) {
        if let ItemKind::Static(..) = item.kind
            && let Some(macro_call) = clippy_utils::macros::root_macro_call(item.span)
            && paths::LAZY_STATIC.matches(cx, macro_call.def_id)
            && can_use_lazy_cell(cx, self.msrv)
        {
            span_lint(
                cx,
                NON_STD_LAZY_STATICS,
                macro_call.span,
                "this macro has been superseded by `std::sync::LazyLock`",
            );
            return;
        }

        if item.span.in_external_macro(cx.sess().source_map()) {
            return;
        }

        if let Some(lazy_info) = LazyInfo::from_item(cx, item)
            && can_use_lazy_cell(cx, self.msrv)
        {
            self.lazy_type_defs.insert(item.owner_id.to_def_id(), lazy_info);
        }
    }

    fn check_expr(&mut self, cx: &LateContext<'hir>, expr: &Expr<'hir>) {
        // All functions in the `FUNCTION_REPLACEMENTS` have only one args
        if let ExprKind::Call(callee, [arg]) = expr.kind
            && let Some(call_def_id) = fn_def_id(cx, expr)
            && self.sugg_map.contains_key(&call_def_id)
            && let ExprKind::Path(qpath) = arg.peel_borrows().kind
            && let Some(arg_def_id) = cx.typeck_results().qpath_res(&qpath, arg.hir_id).opt_def_id()
            && let Some(lazy_info) = self.lazy_type_defs.get_mut(&arg_def_id)
        {
            lazy_info.calls_span_and_id.insert(callee.span, call_def_id);
        }
    }

    fn check_ty(&mut self, cx: &LateContext<'hir>, ty: &'hir rustc_hir::Ty<'hir, rustc_hir::AmbigArg>) {
        // Record if types from `once_cell` besides `sync::Lazy` are used.
        if let rustc_hir::TyKind::Path(qpath) = ty.peel_refs().kind
            && let Some(ty_def_id) = cx.qpath_res(&qpath, ty.hir_id).opt_def_id()
            // Is from `once_cell` crate
            && self.once_cell_crates.contains(&ty_def_id.krate)
            // And is NOT `once_cell::sync::Lazy`
            && !paths::ONCE_CELL_SYNC_LAZY.matches(cx, ty_def_id)
        {
            self.uses_other_once_cell_types = true;
        }
    }

    fn check_crate_post(&mut self, cx: &LateContext<'hir>) {
        if !self.uses_other_once_cell_types {
            for (_, lazy_info) in &self.lazy_type_defs {
                lazy_info.lint(cx, &self.sugg_map);
            }
        }
    }
}

struct LazyInfo {
    /// Span of the [`hir::Ty`] without including args.
    /// i.e.:
    /// ```ignore
    /// static FOO: Lazy<String> = Lazy::new(...);
    /// //          ^^^^
    /// ```
    ty_span_no_args: Span,
    /// Item on which the lint must be generated.
    item_hir_id: HirId,
    /// `Span` and `DefId` of calls on `Lazy` type.
    /// i.e.:
    /// ```ignore
    /// static FOO: Lazy<String> = Lazy::new(...);
    /// //                         ^^^^^^^^^
    /// ```
    calls_span_and_id: FxIndexMap<Span, DefId>,
}

impl LazyInfo {
    fn from_item(cx: &LateContext<'_>, item: &Item<'_>) -> Option<Self> {
        // Check if item is a `once_cell:sync::Lazy` static.
        if let ItemKind::Static(_, _, ty, body_id) = item.kind
            && let Some(path_def_id) = ty.basic_res().opt_def_id()
            && let hir::TyKind::Path(hir::QPath::Resolved(_, path)) = ty.kind
            && paths::ONCE_CELL_SYNC_LAZY.matches(cx, path_def_id)
        {
            let ty_span_no_args = path_span_without_args(path);
            let body = cx.tcx.hir_body(body_id);

            // visit body to collect `Lazy::new` calls
            let mut new_fn_calls = FxIndexMap::default();
            for_each_expr::<(), ()>(cx, body, |ex| {
                if let Some((fn_did, call_span)) = fn_def_id_and_span_from_body(cx, ex, body_id)
                    && paths::ONCE_CELL_SYNC_LAZY_NEW.matches(cx, fn_did)
                {
                    new_fn_calls.insert(call_span, fn_did);
                }
                std::ops::ControlFlow::Continue(())
            });

            Some(LazyInfo {
                ty_span_no_args,
                item_hir_id: item.hir_id(),
                calls_span_and_id: new_fn_calls,
            })
        } else {
            None
        }
    }

    fn lint(&self, cx: &LateContext<'_>, sugg_map: &FxIndexMap<DefId, Option<String>>) {
        // Applicability might get adjusted to `Unspecified` later if any calls
        // in `calls_span_and_id` are not replaceable judging by the `sugg_map`.
        let mut app = Applicability::MachineApplicable;
        let mut suggs = vec![(self.ty_span_no_args, "std::sync::LazyLock".to_string())];

        for (span, def_id) in &self.calls_span_and_id {
            let maybe_sugg = sugg_map.get(def_id).cloned().flatten();
            if let Some(sugg) = maybe_sugg {
                suggs.push((*span, sugg));
            } else {
                // If NO suggested replacement, not machine applicable
                app = Applicability::Unspecified;
            }
        }

        span_lint_hir_and_then(
            cx,
            NON_STD_LAZY_STATICS,
            self.item_hir_id,
            self.ty_span_no_args,
            "this type has been superseded by `LazyLock` in the standard library",
            |diag| {
                diag.multipart_suggestion("use `std::sync::LazyLock` instead", suggs, app);
            },
        );
    }
}

/// Return the span of a given `Path` without including any of its args.
///
/// NB: Re-write of a private function `rustc_lint::non_local_def::path_span_without_args`.
fn path_span_without_args(path: &hir::Path<'_>) -> Span {
    path.segments
        .last()
        .and_then(|seg| seg.args)
        .map_or(path.span, |args| path.span.until(args.span_ext))
}

/// Returns the `DefId` and `Span` of the callee if the given expression is a function call.
///
/// NB: Modified from [`clippy_utils::fn_def_id`], to support calling in an static `Item`'s body.
fn fn_def_id_and_span_from_body(cx: &LateContext<'_>, expr: &Expr<'_>, body_id: BodyId) -> Option<(DefId, Span)> {
    // FIXME: find a way to cache the result.
    let typeck = cx.tcx.typeck_body(body_id);
    match &expr.kind {
        ExprKind::Call(
            Expr {
                kind: ExprKind::Path(qpath),
                hir_id: path_hir_id,
                span,
                ..
            },
            ..,
        ) => {
            // Only return Fn-like DefIds, not the DefIds of statics/consts/etc that contain or
            // deref to fn pointers, dyn Fn, impl Fn - #8850
            if let Res::Def(DefKind::Fn | DefKind::Ctor(..) | DefKind::AssocFn, id) =
                typeck.qpath_res(qpath, *path_hir_id)
            {
                Some((id, *span))
            } else {
                None
            }
        },
        _ => None,
    }
}
