use clippy_utils::diagnostics::span_lint;
use clippy_utils::is_lint_allowed;
use rustc_ast::LitKind;
use rustc_hir as hir;
use rustc_hir::def::DefKind;
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::Ty;
use rustc_session::declare_lint_pass;

declare_clippy_lint! {
    /// ### What it does
    /// Warns about needless / redundant type annotations.
    ///
    /// ### Why restrict this?
    /// Code without type annotations is shorter and in most cases
    /// more idiomatic and easier to modify.
    ///
    /// ### Limitations
    /// This lint doesn't support:
    ///
    /// - Generics
    /// - Refs returned from anything else than a `MethodCall`
    /// - Complex types (tuples, arrays, etc...)
    /// - `Path` to anything else than a primitive type.
    ///
    /// ### Example
    /// ```no_run
    /// let foo: String = String::new();
    /// ```
    /// Use instead:
    /// ```no_run
    /// let foo = String::new();
    /// ```
    #[clippy::version = "1.72.0"]
    pub REDUNDANT_TYPE_ANNOTATIONS,
    restriction,
    "warns about needless / redundant type annotations."
}
declare_lint_pass!(RedundantTypeAnnotations => [REDUNDANT_TYPE_ANNOTATIONS]);

fn is_same_type<'tcx>(cx: &LateContext<'tcx>, ty_resolved_path: hir::def::Res, func_return_type: Ty<'tcx>) -> bool {
    // type annotation is primitive
    if let hir::def::Res::PrimTy(primty) = ty_resolved_path
        && func_return_type.is_primitive()
        && let Some(func_return_type_sym) = func_return_type.primitive_symbol()
    {
        return primty.name() == func_return_type_sym;
    }

    // type annotation is a non generic type
    if let hir::def::Res::Def(DefKind::Struct | DefKind::Union | DefKind::Enum, defid) = ty_resolved_path
        && let Some(annotation_ty) = cx.tcx.type_of(defid).no_bound_vars()
    {
        return annotation_ty == func_return_type;
    }

    false
}

fn func_hir_id_to_func_ty<'tcx>(cx: &LateContext<'tcx>, hir_id: hir::hir_id::HirId) -> Option<Ty<'tcx>> {
    if let Some((defkind, func_defid)) = cx.typeck_results().type_dependent_def(hir_id)
        && defkind == DefKind::AssocFn
        && let Some(init_ty) = cx.tcx.type_of(func_defid).no_bound_vars()
    {
        Some(init_ty)
    } else {
        None
    }
}

fn func_ty_to_return_type<'tcx>(cx: &LateContext<'tcx>, func_ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
    if func_ty.is_fn() {
        Some(func_ty.fn_sig(cx.tcx).output().skip_binder())
    } else {
        None
    }
}

/// Extracts the fn Ty, e.g. `fn() -> std::string::String {f}`
fn extract_fn_ty<'tcx>(
    cx: &LateContext<'tcx>,
    call: &hir::Expr<'tcx>,
    func_return_path: &hir::QPath<'tcx>,
) -> Option<Ty<'tcx>> {
    match func_return_path {
        // let a: String = f(); where f: fn f() -> String
        hir::QPath::Resolved(_, resolved_path) => {
            if let hir::def::Res::Def(_, defid) = resolved_path.res
                && let Some(middle_ty_init) = cx.tcx.type_of(defid).no_bound_vars()
            {
                Some(middle_ty_init)
            } else {
                None
            }
        },
        // Associated functions like
        // let a: String = String::new();
        // let a: String = String::get_string();
        hir::QPath::TypeRelative(..) => func_hir_id_to_func_ty(cx, call.hir_id),
        hir::QPath::LangItem(..) => None,
    }
}

fn is_redundant_in_func_call<'tcx>(
    cx: &LateContext<'tcx>,
    ty_resolved_path: hir::def::Res,
    call: &hir::Expr<'tcx>,
) -> bool {
    if let hir::ExprKind::Path(init_path) = &call.kind {
        let func_type = extract_fn_ty(cx, call, init_path);

        if let Some(func_type) = func_type
            && let Some(init_return_type) = func_ty_to_return_type(cx, func_type)
        {
            return is_same_type(cx, ty_resolved_path, init_return_type);
        }
    }

    false
}

fn extract_primty(ty_kind: &hir::TyKind<'_>) -> Option<hir::PrimTy> {
    if let hir::TyKind::Path(ty_path) = ty_kind
        && let hir::QPath::Resolved(_, resolved_path_ty) = ty_path
        && let hir::def::Res::PrimTy(primty) = resolved_path_ty.res
    {
        Some(primty)
    } else {
        None
    }
}

impl LateLintPass<'_> for RedundantTypeAnnotations {
    fn check_local<'tcx>(&mut self, cx: &LateContext<'tcx>, local: &'tcx rustc_hir::LetStmt<'tcx>) {
        if !is_lint_allowed(cx, REDUNDANT_TYPE_ANNOTATIONS, local.hir_id)
            // type annotation part
            && !local.span.from_expansion()
            && let Some(ty) = &local.ty

            // initialization part
            && let Some(init) = local.init
        {
            match &init.kind {
                // When the initialization is a call to a function
                hir::ExprKind::Call(init_call, _) => {
                    if let hir::TyKind::Path(ty_path) = &ty.kind
                        && let hir::QPath::Resolved(_, resolved_path_ty) = ty_path
                        && is_redundant_in_func_call(cx, resolved_path_ty.res, init_call)
                    {
                        span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation");
                    }
                },
                hir::ExprKind::MethodCall(_, _, _, _) => {
                    let mut is_ref = false;
                    let mut ty_kind = &ty.kind;

                    // If the annotation is a ref we "peel" it
                    if let hir::TyKind::Ref(_, mut_ty) = &ty.kind {
                        is_ref = true;
                        ty_kind = &mut_ty.ty.kind;
                    }

                    if let hir::TyKind::Path(ty_path) = ty_kind
                        && let hir::QPath::Resolved(_, resolved_path_ty) = ty_path
                        && let Some(func_ty) = func_hir_id_to_func_ty(cx, init.hir_id)
                        && let Some(return_type) = func_ty_to_return_type(cx, func_ty)
                        && is_same_type(
                            cx,
                            resolved_path_ty.res,
                            if is_ref { return_type.peel_refs() } else { return_type },
                        )
                    {
                        span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation");
                    }
                },
                // When the initialization is a path for example u32::MAX
                hir::ExprKind::Path(init_path) => {
                    // TODO: check for non primty
                    if let Some(primty) = extract_primty(&ty.kind)
                        && let hir::QPath::TypeRelative(init_ty, _) = init_path
                        && let Some(primty_init) = extract_primty(&init_ty.kind)
                        && primty == primty_init
                    {
                        span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation");
                    }
                },
                hir::ExprKind::Lit(init_lit) => {
                    match init_lit.node {
                        // In these cases the annotation is redundant
                        LitKind::Str(..)
                        | LitKind::Byte(..)
                        | LitKind::Char(..)
                        | LitKind::Bool(..)
                        | LitKind::CStr(..) => {
                            span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation");
                        },
                        LitKind::Int(..) | LitKind::Float(..) => {
                            // If the initialization value is a suffixed literal we lint
                            if init_lit.node.is_suffixed() {
                                span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation");
                            }
                        },
                        LitKind::Err(_) => (),
                        LitKind::ByteStr(..) => {
                            // We only lint if the type annotation is an array type (e.g. &[u8; 4]).
                            // If instead it is a slice (e.g. &[u8]) it may not be redundant, so we
                            // don't lint.
                            if let hir::TyKind::Ref(_, mut_ty) = ty.kind
                                && matches!(mut_ty.ty.kind, hir::TyKind::Array(..))
                            {
                                span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation");
                            }
                        },
                    }
                },
                _ => (),
            }
        }
    }
}
