use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::higher::VecArgs;
use clippy_utils::source::{snippet, snippet_indent};
use rustc_ast::LitKind;
use rustc_data_structures::packed::Pu128;
use rustc_errors::Applicability;
use rustc_hir::{ConstArgKind, Expr, ExprKind, LetStmt, LocalSource, Node};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{IsSuggestable, Ty};
use rustc_session::declare_lint_pass;
use rustc_span::Span;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for array or vec initializations which contain an expression with side effects,
    /// but which have a repeat count of zero.
    ///
    /// ### Why is this bad?
    /// Such an initialization, despite having a repeat length of 0, will still call the inner function.
    /// This may not be obvious and as such there may be unintended side effects in code.
    ///
    /// ### Example
    /// ```no_run
    /// fn side_effect() -> i32 {
    ///     println!("side effect");
    ///     10
    /// }
    /// let a = [side_effect(); 0];
    /// ```
    /// Use instead:
    /// ```no_run
    /// fn side_effect() -> i32 {
    ///     println!("side effect");
    ///     10
    /// }
    /// side_effect();
    /// let a: [i32; 0] = [];
    /// ```
    #[clippy::version = "1.79.0"]
    pub ZERO_REPEAT_SIDE_EFFECTS,
    suspicious,
    "usage of zero-sized initializations of arrays or vecs causing side effects"
}

declare_lint_pass!(ZeroRepeatSideEffects => [ZERO_REPEAT_SIDE_EFFECTS]);

impl LateLintPass<'_> for ZeroRepeatSideEffects {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        if let Some(args) = VecArgs::hir(cx, expr)
            && let VecArgs::Repeat(inner_expr, len) = args
            && let ExprKind::Lit(l) = len.kind
            && let LitKind::Int(Pu128(0), _) = l.node
        {
            inner_check(cx, expr, inner_expr, true);
        }
        // Lint only if the length is a literal zero, and not a path to any constants.
        // NOTE(@y21): When reading `[f(); LEN]`, I intuitively expect that the function is called and it
        // doesn't seem as confusing as `[f(); 0]`. It would also have false positives when eg.
        // the const item depends on `#[cfg]s` and has different values in different compilation
        // sessions).
        else if let ExprKind::Repeat(inner_expr, const_arg) = expr.kind
            && let ConstArgKind::Anon(anon_const) = const_arg.kind
            && let length_expr = cx.tcx.hir_body(anon_const.body).value
            && !length_expr.span.from_expansion()
            && let ExprKind::Lit(literal) = length_expr.kind
            && let LitKind::Int(Pu128(0), _) = literal.node
        {
            inner_check(cx, expr, inner_expr, false);
        }
    }
}

fn inner_check(cx: &LateContext<'_>, expr: &'_ Expr<'_>, inner_expr: &'_ Expr<'_>, is_vec: bool) {
    // check if expr is a call or has a call inside it
    if inner_expr.can_have_side_effects() {
        let parent_hir_node = cx.tcx.parent_hir_node(expr.hir_id);
        let inner_expr_ty = cx.typeck_results().expr_ty(inner_expr);
        let return_type = cx.typeck_results().expr_ty(expr);

        let inner_expr = snippet(cx, inner_expr.span.source_callsite(), "..");
        let indent = snippet_indent(cx, expr.span).unwrap_or_default();
        let vec = if is_vec { "vec!" } else { "" };

        let (span, sugg) = match parent_hir_node {
            Node::LetStmt(l)
                if matches!(l.source, LocalSource::AssignDesugar)
                    && let mut parent_iter = cx.tcx.hir_parent_iter(l.hir_id)
                    && let Some((_, Node::Stmt(_))) = parent_iter.next()
                    && let Some((_, Node::Block(_))) = parent_iter.next()
                    && let Some((_, Node::Expr(x))) = parent_iter.next() =>
            {
                (
                    x.span,
                    assign_expr_suggestion(cx, x, l.pat.span, &inner_expr, return_type, vec),
                )
            },
            Node::LetStmt(l) => (l.span, let_stmt_suggestion(cx, l, &inner_expr, return_type, vec)),
            Node::Expr(x) if let ExprKind::Assign(l, _, _) = x.kind => (
                x.span,
                assign_expr_suggestion(cx, x, l.span, &inner_expr, return_type, vec),
            ),
            // NOTE: don't use the stmt span to avoid touching the trailing semicolon
            Node::Stmt(_) => (expr.span, format!("{inner_expr};\n{indent}{vec}[] as {return_type}")),
            _ => (
                expr.span,
                format!(
                    "\
{{
{indent}    {inner_expr};
{indent}    {vec}[] as {return_type}
{indent}}}"
                ),
            ),
        };
        let span = span.source_callsite();
        span_lint_and_then(
            cx,
            ZERO_REPEAT_SIDE_EFFECTS,
            span,
            "expression with side effects as the initial value in a zero-sized array initializer",
            |diag| {
                if (!inner_expr_ty.is_never() || cx.tcx.features().never_type())
                    && return_type.is_suggestable(cx.tcx, true)
                {
                    diag.span_suggestion_verbose(
                        span,
                        "consider performing the side effect separately",
                        sugg,
                        Applicability::Unspecified,
                    );
                } else {
                    diag.help("consider performing the side effect separately");
                }
            },
        );
    }
}

fn let_stmt_suggestion(
    cx: &LateContext<'_>,
    let_stmt: &LetStmt<'_>,
    inner_expr: &str,
    return_type: Ty<'_>,
    vec_str: &str,
) -> String {
    let indent = snippet_indent(cx, let_stmt.span).unwrap_or_default();
    format!(
        "{inner_expr};\n{}let {var_name}: {return_type} = {vec_str}[];",
        indent,
        var_name = snippet(cx, let_stmt.pat.span.source_callsite(), "..")
    )
}

fn assign_expr_suggestion(
    cx: &LateContext<'_>,
    outer_expr: &Expr<'_>,
    assign_expr_span: Span,
    inner_expr: &str,
    return_type: Ty<'_>,
    vec_str: &str,
) -> String {
    let mut parent_hir_node = cx.tcx.parent_hir_node(outer_expr.hir_id);
    if let Node::Stmt(stmt) = parent_hir_node {
        parent_hir_node = cx.tcx.parent_hir_node(stmt.hir_id);
    }
    let needs_curly = !matches!(parent_hir_node, Node::Block(_));

    let indent = snippet_indent(cx, outer_expr.span).unwrap_or_default();
    let var_name = snippet(cx, assign_expr_span.source_callsite(), "..");
    if needs_curly {
        format!("{{\n    {indent}{inner_expr};\n    {indent}{var_name} = {vec_str}[] as {return_type}\n{indent}}}")
    } else {
        format!("{inner_expr};\n{indent}{var_name} = {vec_str}[] as {return_type}")
    }
}
