use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::source::snippet;
use clippy_utils::{get_parent_expr, sym};
use rustc_ast::{LitKind, StrStyle};
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, Node, QPath, TyKind};
use rustc_lint::LateContext;
use rustc_span::edition::Edition::Edition2021;
use rustc_span::{Span, Symbol};

use super::MANUAL_C_STR_LITERALS;

/// Checks:
/// - `b"...".as_ptr()`
/// - `b"...".as_ptr().cast()`
/// - `"...".as_ptr()`
/// - `"...".as_ptr().cast()`
///
/// Iff the parent call of `.cast()` isn't `CStr::from_ptr`, to avoid linting twice.
pub(super) fn check_as_ptr<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &'tcx Expr<'tcx>,
    receiver: &'tcx Expr<'tcx>,
    msrv: Msrv,
) {
    if let ExprKind::Lit(lit) = receiver.kind
        && let LitKind::ByteStr(_, StrStyle::Cooked) | LitKind::Str(_, StrStyle::Cooked) = lit.node
        && cx.tcx.sess.edition() >= Edition2021
        && let casts_removed = peel_ptr_cast_ancestors(cx, expr)
        && !get_parent_expr(cx, casts_removed).is_some_and(
            |parent| matches!(parent.kind, ExprKind::Call(func, _) if is_c_str_function(cx, func).is_some()),
        )
        && let Some(sugg) = rewrite_as_cstr(cx, lit.span)
        && msrv.meets(cx, msrvs::C_STR_LITERALS)
    {
        span_lint_and_sugg(
            cx,
            MANUAL_C_STR_LITERALS,
            receiver.span,
            "manually constructing a nul-terminated string",
            r#"use a `c""` literal"#,
            sugg,
            // an additional cast may be needed, since the type of `CStr::as_ptr` and
            // `"".as_ptr()` can differ and is platform dependent
            Applicability::HasPlaceholders,
        );
    }
}

/// Checks if the callee is a "relevant" `CStr` function considered by this lint.
/// Returns the function name.
fn is_c_str_function(cx: &LateContext<'_>, func: &Expr<'_>) -> Option<Symbol> {
    if let ExprKind::Path(QPath::TypeRelative(cstr, fn_name)) = &func.kind
        && let TyKind::Path(QPath::Resolved(_, ty_path)) = &cstr.kind
        && cx.tcx.lang_items().c_str() == ty_path.res.opt_def_id()
    {
        Some(fn_name.ident.name)
    } else {
        None
    }
}

/// Checks calls to the `CStr` constructor functions:
/// - `CStr::from_bytes_with_nul(..)`
/// - `CStr::from_bytes_with_nul_unchecked(..)`
/// - `CStr::from_ptr(..)`
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, func: &Expr<'_>, args: &[Expr<'_>], msrv: Msrv) {
    if let Some(fn_name) = is_c_str_function(cx, func)
        && let [arg] = args
        && cx.tcx.sess.edition() >= Edition2021
        && msrv.meets(cx, msrvs::C_STR_LITERALS)
    {
        match fn_name {
            sym::from_bytes_with_nul | sym::from_bytes_with_nul_unchecked
                if !arg.span.from_expansion()
                    && let ExprKind::Lit(lit) = arg.kind
                    && let LitKind::ByteStr(_, StrStyle::Cooked) | LitKind::Str(_, StrStyle::Cooked) = lit.node =>
            {
                check_from_bytes(cx, expr, arg, fn_name);
            },
            sym::from_ptr => check_from_ptr(cx, expr, arg),
            _ => {},
        }
    }
}

/// Checks `CStr::from_ptr(b"foo\0".as_ptr().cast())`
fn check_from_ptr(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>) {
    if let ExprKind::MethodCall(method, lit, [], _) = peel_ptr_cast(arg).kind
        && method.ident.name == sym::as_ptr
        && !lit.span.from_expansion()
        && let ExprKind::Lit(lit) = lit.kind
        && let LitKind::ByteStr(_, StrStyle::Cooked) = lit.node
        && let Some(sugg) = rewrite_as_cstr(cx, lit.span)
    {
        span_lint_and_sugg(
            cx,
            MANUAL_C_STR_LITERALS,
            expr.span,
            "calling `CStr::from_ptr` with a byte string literal",
            r#"use a `c""` literal"#,
            sugg,
            Applicability::MachineApplicable,
        );
    }
}
/// Checks `CStr::from_bytes_with_nul(b"foo\0")`
fn check_from_bytes(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, method: Symbol) {
    let (span, applicability) = if let Some(parent) = get_parent_expr(cx, expr)
        && let ExprKind::MethodCall(method, ..) = parent.kind
        && [sym::unwrap, sym::expect].contains(&method.ident.name)
    {
        (parent.span, Applicability::MachineApplicable)
    } else if method == sym::from_bytes_with_nul_unchecked {
        // `*_unchecked` returns `&CStr` directly, nothing needs to be changed
        (expr.span, Applicability::MachineApplicable)
    } else {
        // User needs to remove error handling, can't be machine applicable
        (expr.span, Applicability::HasPlaceholders)
    };

    let Some(sugg) = rewrite_as_cstr(cx, arg.span) else {
        return;
    };

    span_lint_and_sugg(
        cx,
        MANUAL_C_STR_LITERALS,
        span,
        "calling `CStr::new` with a byte string literal",
        r#"use a `c""` literal"#,
        sugg,
        applicability,
    );
}

/// Rewrites a byte string literal to a c-str literal.
/// `b"foo\0"` -> `c"foo"`
///
/// Returns `None` if it doesn't end in a NUL byte.
fn rewrite_as_cstr(cx: &LateContext<'_>, span: Span) -> Option<String> {
    let mut sugg = String::from("c") + snippet(cx, span.source_callsite(), "..").trim_start_matches('b');

    // NUL byte should always be right before the closing quote.
    if let Some(quote_pos) = sugg.rfind('"') {
        // Possible values right before the quote:
        // - literal NUL value
        if sugg.as_bytes()[quote_pos - 1] == b'\0' {
            sugg.remove(quote_pos - 1);
        }
        // - \x00
        else if sugg[..quote_pos].ends_with("\\x00") {
            sugg.replace_range(quote_pos - 4..quote_pos, "");
        }
        // - \0
        else if sugg[..quote_pos].ends_with("\\0") {
            sugg.replace_range(quote_pos - 2..quote_pos, "");
        }
        // No known suffix, so assume it's not a C-string.
        else {
            return None;
        }
    }

    Some(sugg)
}

fn get_cast_target<'tcx>(e: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
    match &e.kind {
        ExprKind::MethodCall(method, receiver, [], _) if method.ident.name == sym::cast => Some(receiver),
        ExprKind::Cast(expr, _) => Some(expr),
        _ => None,
    }
}

/// `x.cast()` -> `x`
/// `x as *const _` -> `x`
/// `x` -> `x` (returns the same expression for non-cast exprs)
fn peel_ptr_cast<'tcx>(e: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
    get_cast_target(e).map_or(e, peel_ptr_cast)
}

/// Same as `peel_ptr_cast`, but the other way around, by walking up the ancestor cast expressions:
///
/// `foo(x.cast() as *const _)`
///      ^ given this `x` expression, returns the `foo(...)` expression
fn peel_ptr_cast_ancestors<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
    let mut prev = e;
    for (_, node) in cx.tcx.hir_parent_iter(e.hir_id) {
        if let Node::Expr(e) = node
            && get_cast_target(e).is_some()
        {
            prev = e;
        } else {
            break;
        }
    }
    prev
}
