use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
use clippy_utils::macros::{FormatArgsStorage, find_format_arg_expr, is_format_macro, root_macro_call_first_node};
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::{get_parent_as_impl, peel_ref_operators, sym};
use rustc_ast::{FormatArgsPiece, FormatTrait};
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, Impl, ImplItem, ImplItemKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::Symbol;
use rustc_span::symbol::kw;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for format trait implementations (e.g. `Display`) with a recursive call to itself
    /// which uses `self` as a parameter.
    /// This is typically done indirectly with the `write!` macro or with `to_string()`.
    ///
    /// ### Why is this bad?
    /// This will lead to infinite recursion and a stack overflow.
    ///
    /// ### Example
    ///
    /// ```no_run
    /// use std::fmt;
    ///
    /// struct Structure(i32);
    /// impl fmt::Display for Structure {
    ///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    ///         write!(f, "{}", self.to_string())
    ///     }
    /// }
    ///
    /// ```
    /// Use instead:
    /// ```no_run
    /// use std::fmt;
    ///
    /// struct Structure(i32);
    /// impl fmt::Display for Structure {
    ///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    ///         write!(f, "{}", self.0)
    ///     }
    /// }
    /// ```
    #[clippy::version = "1.48.0"]
    pub RECURSIVE_FORMAT_IMPL,
    correctness,
    "Format trait method called while implementing the same Format trait"
}

declare_clippy_lint! {
    /// ### What it does
    /// Checks for usage of `println`, `print`, `eprintln` or `eprint` in an
    /// implementation of a formatting trait.
    ///
    /// ### Why is this bad?
    /// Using a print macro is likely unintentional since formatting traits
    /// should write to the `Formatter`, not stdout/stderr.
    ///
    /// ### Example
    /// ```no_run
    /// use std::fmt::{Display, Error, Formatter};
    ///
    /// struct S;
    /// impl Display for S {
    ///     fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
    ///         println!("S");
    ///
    ///         Ok(())
    ///     }
    /// }
    /// ```
    /// Use instead:
    /// ```no_run
    /// use std::fmt::{Display, Error, Formatter};
    ///
    /// struct S;
    /// impl Display for S {
    ///     fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
    ///         writeln!(f, "S");
    ///
    ///         Ok(())
    ///     }
    /// }
    /// ```
    #[clippy::version = "1.61.0"]
    pub PRINT_IN_FORMAT_IMPL,
    suspicious,
    "use of a print macro in a formatting trait impl"
}

#[derive(Clone, Copy)]
struct FormatTraitNames {
    /// e.g. `sym::Display`
    name: Symbol,
    /// `f` in `fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {}`
    formatter_name: Option<Symbol>,
}

pub struct FormatImpl {
    format_args: FormatArgsStorage,
    // Whether we are inside Display or Debug trait impl - None for neither
    format_trait_impl: Option<FormatTraitNames>,
}

impl FormatImpl {
    pub fn new(format_args: FormatArgsStorage) -> Self {
        Self {
            format_args,
            format_trait_impl: None,
        }
    }
}

impl_lint_pass!(FormatImpl => [RECURSIVE_FORMAT_IMPL, PRINT_IN_FORMAT_IMPL]);

impl<'tcx> LateLintPass<'tcx> for FormatImpl {
    fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) {
        self.format_trait_impl = is_format_trait_impl(cx, impl_item);
    }

    fn check_impl_item_post(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) {
        // Assume no nested Impl of Debug and Display within each other
        if is_format_trait_impl(cx, impl_item).is_some() {
            self.format_trait_impl = None;
        }
    }

    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
        if let Some(format_trait_impl) = self.format_trait_impl {
            let linter = FormatImplExpr {
                cx,
                format_args: &self.format_args,
                expr,
                format_trait_impl,
            };
            linter.check_to_string_in_display();
            linter.check_self_in_format_args();
            linter.check_print_in_format_impl();
        }
    }
}

struct FormatImplExpr<'a, 'tcx> {
    cx: &'a LateContext<'tcx>,
    format_args: &'a FormatArgsStorage,
    expr: &'tcx Expr<'tcx>,
    format_trait_impl: FormatTraitNames,
}

impl FormatImplExpr<'_, '_> {
    fn check_to_string_in_display(&self) {
        if self.format_trait_impl.name == sym::Display
            && let ExprKind::MethodCall(path, self_arg, [], _) = self.expr.kind
            // Get the hir_id of the object we are calling the method on
            // Is the method to_string() ?
            && path.ident.name == sym::to_string
            // Is the method a part of the ToString trait? (i.e. not to_string() implemented
            // separately)
            && self
                .cx
                .typeck_results()
                .type_dependent_def_id(self.expr.hir_id)
                .opt_parent(self.cx)
                .is_diag_item(self.cx, sym::ToString)
            // Is the method is called on self
            && let ExprKind::Path(QPath::Resolved(_, path)) = self_arg.kind
            && let [segment] = path.segments
            && segment.ident.name == kw::SelfLower
        {
            span_lint(
                self.cx,
                RECURSIVE_FORMAT_IMPL,
                self.expr.span,
                "using `self.to_string` in `fmt::Display` implementation will cause infinite recursion",
            );
        }
    }

    fn check_self_in_format_args(&self) {
        // Check each arg in format calls - do we ever use Display on self (directly or via deref)?
        if let Some(outer_macro) = root_macro_call_first_node(self.cx, self.expr)
            && let macro_def_id = outer_macro.def_id
            && is_format_macro(self.cx, macro_def_id)
            && let Some(format_args) = self.format_args.get(self.cx, self.expr, outer_macro.expn)
        {
            for piece in &format_args.template {
                if let FormatArgsPiece::Placeholder(placeholder) = piece
                    && let trait_name = match placeholder.format_trait {
                        FormatTrait::Display => sym::Display,
                        FormatTrait::Debug => sym::Debug,
                        FormatTrait::LowerExp => sym::LowerExp,
                        FormatTrait::UpperExp => sym::UpperExp,
                        FormatTrait::Octal => sym::Octal,
                        FormatTrait::Pointer => sym::Pointer,
                        FormatTrait::Binary => sym::Binary,
                        FormatTrait::LowerHex => sym::LowerHex,
                        FormatTrait::UpperHex => sym::UpperHex,
                    }
                    && trait_name == self.format_trait_impl.name
                    && let Ok(index) = placeholder.argument.index
                    && let Some(arg) = format_args.arguments.all_args().get(index)
                    && let Some(arg_expr) = find_format_arg_expr(self.expr, arg)
                {
                    self.check_format_arg_self(arg_expr);
                }
            }
        }
    }

    fn check_format_arg_self(&self, arg: &Expr<'_>) {
        // Handle multiple dereferencing of references e.g. &&self
        // Handle dereference of &self -> self that is equivalent (i.e. via *self in fmt() impl)
        // Since the argument to fmt is itself a reference: &self
        let reference = peel_ref_operators(self.cx, arg);
        // Is the reference self?
        if reference.res_local_id().map(|x| self.cx.tcx.hir_name(x)) == Some(kw::SelfLower) {
            let FormatTraitNames { name, .. } = self.format_trait_impl;
            span_lint(
                self.cx,
                RECURSIVE_FORMAT_IMPL,
                self.expr.span,
                format!("using `self` as `{name}` in `impl {name}` will cause infinite recursion"),
            );
        }
    }

    fn check_print_in_format_impl(&self) {
        if let Some(macro_call) = root_macro_call_first_node(self.cx, self.expr)
            && let Some(name) = self.cx.tcx.get_diagnostic_name(macro_call.def_id)
        {
            let replacement = match name {
                sym::print_macro | sym::eprint_macro => "write",
                sym::println_macro | sym::eprintln_macro => "writeln",
                _ => return,
            };

            let name = name.as_str().strip_suffix("_macro").unwrap();

            span_lint_and_sugg(
                self.cx,
                PRINT_IN_FORMAT_IMPL,
                macro_call.span,
                format!("use of `{name}!` in `{}` impl", self.format_trait_impl.name),
                "replace with",
                if let Some(formatter_name) = self.format_trait_impl.formatter_name {
                    format!("{replacement}!({formatter_name}, ..)")
                } else {
                    format!("{replacement}!(..)")
                },
                Applicability::HasPlaceholders,
            );
        }
    }
}

fn is_format_trait_impl(cx: &LateContext<'_>, impl_item: &ImplItem<'_>) -> Option<FormatTraitNames> {
    if impl_item.ident.name == sym::fmt
        && let ImplItemKind::Fn(_, body_id) = impl_item.kind
        && let Some(Impl {
            of_trait: Some(of_trait),
            ..
        }) = get_parent_as_impl(cx.tcx, impl_item.hir_id())
        && let Some(did) = of_trait.trait_ref.trait_def_id()
        && let Some(name) = cx.tcx.get_diagnostic_name(did)
        && matches!(name, sym::Debug | sym::Display)
    {
        let body = cx.tcx.hir_body(body_id);
        let formatter_name = body
            .params
            .get(1)
            .and_then(|param| param.pat.simple_ident())
            .map(|ident| ident.name);

        Some(FormatTraitNames { name, formatter_name })
    } else {
        None
    }
}
