use std::ops::Range;

use parse::Position::ArgumentNamed;
use rustc_ast::tokenstream::TokenStream;
use rustc_ast::{
    Expr, ExprKind, FormatAlignment, FormatArgPosition, FormatArgPositionKind, FormatArgs,
    FormatArgsPiece, FormatArgument, FormatArgumentKind, FormatArguments, FormatCount,
    FormatDebugHex, FormatOptions, FormatPlaceholder, FormatSign, FormatTrait, Recovered, StmtKind,
    token,
};
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::{
    Applicability, BufferedEarlyLint, DecorateDiagCompat, Diag, Diagnostic, MultiSpan, PResult,
    SingleLabelManySpans, listify, pluralize,
};
use rustc_expand::base::*;
use rustc_lint_defs::LintId;
use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY;
use rustc_parse::exp;
use rustc_parse_format as parse;
use rustc_span::{BytePos, ErrorGuaranteed, Ident, InnerSpan, Span, Symbol};

use crate::errors;
use crate::util::{ExprToSpannedString, expr_to_spanned_string};

// The format_args!() macro is expanded in three steps:
//  1. First, `parse_args` will parse the `(literal, arg, arg, name=arg, name=arg)` syntax,
//     but doesn't parse the template (the literal) itself.
//  2. Second, `make_format_args` will parse the template, the format options, resolve argument references,
//     produce diagnostics, and turn the whole thing into a `FormatArgs` AST node.
//  3. Much later, in AST lowering (rustc_ast_lowering), that `FormatArgs` structure will be turned
//     into the expression of type `core::fmt::Arguments`.

// See rustc_ast/src/format.rs for the FormatArgs structure and glossary.

// Only used in parse_args and report_invalid_references,
// to indicate how a referred argument was used.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PositionUsedAs {
    Placeholder(Option<Span>),
    Precision,
    Width,
}
use PositionUsedAs::*;

#[derive(Debug)]
struct MacroInput {
    fmtstr: Box<Expr>,
    args: FormatArguments,
    /// Whether the first argument was a string literal or a result from eager macro expansion.
    /// If it's not a string literal, we disallow implicit argument capturing.
    ///
    /// This does not correspond to whether we can treat spans to the literal normally, as the whole
    /// invocation might be the result of another macro expansion, in which case this flag may still be true.
    ///
    /// See [RFC 2795] for more information.
    ///
    /// [RFC 2795]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html#macro-hygiene
    is_direct_literal: bool,
}

/// Parses the arguments from the given list of tokens, returning the diagnostic
/// if there's a parse error so we can continue parsing other format!
/// expressions.
///
/// If parsing succeeds, the return value is:
///
/// ```text
/// Ok((fmtstr, parsed arguments))
/// ```
fn parse_args<'a>(ecx: &ExtCtxt<'a>, sp: Span, tts: TokenStream) -> PResult<'a, MacroInput> {
    let mut p = ecx.new_parser_from_tts(tts);

    // parse the format string
    let fmtstr = match p.token.kind {
        token::Eof => return Err(ecx.dcx().create_err(errors::FormatRequiresString { span: sp })),
        // This allows us to properly handle cases when the first comma
        // after the format string is mistakenly replaced with any operator,
        // which cause the expression parser to eat too much tokens.
        token::Literal(token::Lit { kind: token::Str | token::StrRaw(_), .. }) => {
            p.parse_literal_maybe_minus()?
        }
        // Otherwise, we fall back to the expression parser.
        _ => p.parse_expr()?,
    };

    // parse comma FormatArgument pairs
    let mut args = FormatArguments::new();
    let mut first = true;
    while p.token != token::Eof {
        // parse a comma, or else report an error
        if !p.eat(exp!(Comma)) {
            if first {
                p.clear_expected_token_types();
            }

            match p.expect(exp!(Comma)) {
                Err(err) => {
                    if token::TokenKind::Comma.similar_tokens().contains(&p.token.kind) {
                        // If a similar token is found, then it may be a typo. We
                        // consider it as a comma, and continue parsing.
                        err.emit();
                        p.bump();
                    } else {
                        // Otherwise stop the parsing and return the error.
                        return Err(err);
                    }
                }
                Ok(Recovered::Yes(_)) => (),
                Ok(Recovered::No) => unreachable!(),
            }
        }
        first = false;
        // accept a trailing comma
        if p.token == token::Eof {
            break;
        }
        // parse a FormatArgument
        match p.token.ident() {
            Some((ident, _)) if p.look_ahead(1, |t| *t == token::Eq) => {
                p.bump();
                p.expect(exp!(Eq))?;
                let expr = p.parse_expr()?;
                if let Some((_, prev)) = args.by_name(ident.name) {
                    ecx.dcx().emit_err(errors::FormatDuplicateArg {
                        span: ident.span,
                        prev: prev.kind.ident().unwrap().span,
                        duplicate: ident.span,
                        ident,
                    });
                    continue;
                }
                args.add(FormatArgument { kind: FormatArgumentKind::Named(ident), expr });
            }
            _ => {
                let expr = p.parse_expr()?;
                if !args.named_args().is_empty() {
                    return Err(ecx.dcx().create_err(errors::PositionalAfterNamed {
                        span: expr.span,
                        args: args
                            .named_args()
                            .iter()
                            .filter_map(|a| a.kind.ident().map(|ident| (a, ident)))
                            .map(|(arg, n)| n.span.to(arg.expr.span))
                            .collect(),
                    }));
                }
                args.add(FormatArgument { kind: FormatArgumentKind::Normal, expr });
            }
        }
    }

    // Only allow implicit captures for direct literals
    let is_direct_literal = matches!(fmtstr.kind, ExprKind::Lit(_));

    Ok(MacroInput { fmtstr, args, is_direct_literal })
}

fn make_format_args(
    ecx: &mut ExtCtxt<'_>,
    input: MacroInput,
    append_newline: bool,
    macro_span: Span,
) -> ExpandResult<Result<FormatArgs, ErrorGuaranteed>, ()> {
    let msg = "format argument must be a string literal";
    let unexpanded_fmt_span = input.fmtstr.span;

    let MacroInput { fmtstr: efmt, mut args, is_direct_literal } = input;

    let ExprToSpannedString {
        symbol: fmt_str,
        span: fmt_span,
        style: fmt_style,
        uncooked_symbol: uncooked_fmt_str,
    } = {
        // Extract snippet so that we can check cases `{}`, `{:?}` and `{:#?}` and emit help for
        // them later.
        let snippet = if let ExprKind::Block(b, None) = &efmt.kind
            && b.stmts.len() <= 1
        {
            Some(ecx.sess.source_map().span_to_snippet(unexpanded_fmt_span))
        } else {
            None
        };

        let ExpandResult::Ready(mac) = expr_to_spanned_string(ecx, efmt.clone(), msg) else {
            return ExpandResult::Retry(());
        };
        match mac {
            Ok(mut fmt) if append_newline => {
                fmt.symbol = Symbol::intern(&format!("{}\n", fmt.symbol));
                fmt
            }
            Ok(fmt) => fmt,
            Err(err) => {
                let guar = match err {
                    Ok((mut err, suggested)) => {
                        if !suggested {
                            if let ExprKind::Block(block, None) = &efmt.kind
                                && let [stmt] = block.stmts.as_slice()
                                && let StmtKind::Expr(expr) = &stmt.kind
                                && let ExprKind::Path(None, path) = &expr.kind
                                && path.segments.len() == 1
                                && path.segments[0].args.is_none()
                            {
                                err.multipart_suggestion(
                                    "quote your inlined format argument to use as string literal",
                                    vec![
                                        (unexpanded_fmt_span.shrink_to_hi(), "\"".to_string()),
                                        (unexpanded_fmt_span.shrink_to_lo(), "\"".to_string()),
                                    ],
                                    Applicability::MaybeIncorrect,
                                );
                            } else {
                                // `{}` or `()`
                                let should_suggest = |kind: &ExprKind| -> bool {
                                    match kind {
                                        ExprKind::Block(b, None) if b.stmts.is_empty() => true,
                                        ExprKind::Tup(v) if v.is_empty() => true,
                                        _ => false,
                                    }
                                };

                                let mut sugg_fmt = String::new();
                                for kind in std::iter::once(&efmt.kind)
                                    .chain(args.explicit_args().into_iter().map(|a| &a.expr.kind))
                                {
                                    sugg_fmt.push_str(if should_suggest(kind) {
                                        "{:?} "
                                    } else {
                                        "{} "
                                    });
                                }
                                sugg_fmt = sugg_fmt.trim_end().to_string();
                                err.span_suggestion_verbose(
                                    unexpanded_fmt_span.shrink_to_lo(),
                                    "you might be missing a string literal to format with",
                                    format!("\"{sugg_fmt}\", "),
                                    Applicability::MaybeIncorrect,
                                );

                                if let Some(Ok(snippet)) = snippet.as_ref() {
                                    match snippet.as_str() {
                                        "{}" | "{:?}" | "{:#?}" => {
                                            err.span_suggestion_verbose(
                                                unexpanded_fmt_span,
                                                format!("you might want to enclose `{snippet}` with `\"\"`"),
                                                format!("\"{snippet}\""),
                                                Applicability::MaybeIncorrect,
                                            );
                                        }
                                        _ => {}
                                    };
                                }
                            }
                        }
                        err.emit()
                    }
                    Err(guar) => guar,
                };
                return ExpandResult::Ready(Err(guar));
            }
        }
    };

    let str_style = match fmt_style {
        rustc_ast::StrStyle::Cooked => None,
        rustc_ast::StrStyle::Raw(raw) => Some(raw as usize),
    };

    let fmt_str = fmt_str.as_str(); // for the suggestions below
    let fmt_snippet = ecx.source_map().span_to_snippet(unexpanded_fmt_span).ok();
    let mut parser = parse::Parser::new(
        fmt_str,
        str_style,
        fmt_snippet,
        append_newline,
        parse::ParseMode::Format,
    );

    let mut pieces = Vec::new();
    while let Some(piece) = parser.next() {
        if !parser.errors.is_empty() {
            break;
        } else {
            pieces.push(piece);
        }
    }

    let is_source_literal = parser.is_source_literal;

    if !parser.errors.is_empty() {
        let err = parser.errors.remove(0);
        let sp = if is_source_literal {
            fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end))
        } else {
            // The format string could be another macro invocation, e.g.:
            //     format!(concat!("abc", "{}"), 4);
            // However, `err.span` is an inner span relative to the *result* of
            // the macro invocation, which is why we would get a nonsensical
            // result calling `fmt_span.from_inner(err.span)` as above, and
            // might even end up inside a multibyte character (issue #86085).
            // Therefore, we conservatively report the error for the entire
            // argument span here.
            fmt_span
        };
        let mut e = errors::InvalidFormatString {
            span: sp,
            note_: None,
            label_: None,
            sugg_: None,
            desc: err.description,
            label1: err.label,
        };
        if let Some(note) = err.note {
            e.note_ = Some(errors::InvalidFormatStringNote { note });
        }
        if let Some((label, span)) = err.secondary_label
            && is_source_literal
        {
            e.label_ = Some(errors::InvalidFormatStringLabel {
                span: fmt_span.from_inner(InnerSpan::new(span.start, span.end)),
                label,
            });
        }
        match err.suggestion {
            parse::Suggestion::None => {}
            parse::Suggestion::UsePositional => {
                let captured_arg_span =
                    fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end));
                if let Ok(arg) = ecx.source_map().span_to_snippet(captured_arg_span) {
                    let span = match args.unnamed_args().last() {
                        Some(arg) => arg.expr.span,
                        None => fmt_span,
                    };
                    e.sugg_ = Some(errors::InvalidFormatStringSuggestion::UsePositional {
                        captured: captured_arg_span,
                        len: args.unnamed_args().len().to_string(),
                        span: span.shrink_to_hi(),
                        arg,
                    });
                }
            }
            parse::Suggestion::RemoveRawIdent(span) => {
                if is_source_literal {
                    let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end));
                    e.sugg_ = Some(errors::InvalidFormatStringSuggestion::RemoveRawIdent { span })
                }
            }
            parse::Suggestion::ReorderFormatParameter(span, replacement) => {
                let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end));
                e.sugg_ = Some(errors::InvalidFormatStringSuggestion::ReorderFormatParameter {
                    span,
                    replacement,
                });
            }
            parse::Suggestion::AddMissingColon(span) => {
                let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end));
                e.sugg_ = Some(errors::InvalidFormatStringSuggestion::AddMissingColon { span });
            }
            parse::Suggestion::UseRustDebugPrintingMacro => {
                // This targets `println!("{=}", x);` and `println!("{0=}", x);`
                if let [arg] = args.all_args() {
                    let expr_span = arg.expr.span;
                    if let Ok(expr_snippet) = ecx.source_map().span_to_snippet(expr_span) {
                        let replacement = format!("{}!({})", "dbg", expr_snippet);

                        let call_span = macro_span.source_callsite();
                        e.sugg_ = Some(
                            errors::InvalidFormatStringSuggestion::UseRustDebugPrintingMacro {
                                macro_span: call_span,
                                replacement,
                            },
                        );
                    }
                }
            }
        }
        let guar = ecx.dcx().emit_err(e);
        return ExpandResult::Ready(Err(guar));
    }

    let to_span = |inner_span: Range<usize>| {
        is_source_literal.then(|| {
            fmt_span.from_inner(InnerSpan { start: inner_span.start, end: inner_span.end })
        })
    };

    let mut used = vec![false; args.explicit_args().len()];
    let mut invalid_refs = Vec::new();
    let mut numeric_references_to_named_arg = Vec::new();

    enum ArgRef<'a> {
        Index(usize),
        Name(&'a str, Option<Span>),
    }
    use ArgRef::*;

    let mut unnamed_arg_after_named_arg = false;

    let mut lookup_arg = |arg: ArgRef<'_>,
                          span: Option<Span>,
                          used_as: PositionUsedAs,
                          kind: FormatArgPositionKind|
     -> FormatArgPosition {
        let index = match arg {
            Index(index) => {
                if let Some(arg) = args.by_index(index) {
                    used[index] = true;
                    if arg.kind.ident().is_some() {
                        // This was a named argument, but it was used as a positional argument.
                        numeric_references_to_named_arg.push((index, span, used_as));
                    }
                    Ok(index)
                } else {
                    // Doesn't exist as an explicit argument.
                    invalid_refs.push((index, span, used_as, kind));
                    Err(index)
                }
            }
            Name(name, span) => {
                let name = Symbol::intern(name);
                if let Some((index, _)) = args.by_name(name) {
                    // Name found in `args`, so we resolve it to its index.
                    if index < args.explicit_args().len() {
                        // Mark it as used, if it was an explicit argument.
                        used[index] = true;
                    }
                    Ok(index)
                } else {
                    // Name not found in `args`, so we add it as an implicitly captured argument.
                    let span = span.unwrap_or(fmt_span);
                    let ident = Ident::new(name, span);
                    let expr = if is_direct_literal {
                        ecx.expr_ident(span, ident)
                    } else {
                        // For the moment capturing variables from format strings expanded from macros is
                        // disabled (see RFC #2795)
                        let guar = ecx.dcx().emit_err(errors::FormatNoArgNamed { span, name });
                        unnamed_arg_after_named_arg = true;
                        DummyResult::raw_expr(span, Some(guar))
                    };
                    Ok(args.add(FormatArgument { kind: FormatArgumentKind::Captured(ident), expr }))
                }
            }
        };
        FormatArgPosition { index, kind, span }
    };

    let mut template = Vec::new();
    let mut unfinished_literal = String::new();
    let mut placeholder_index = 0;

    for piece in &pieces {
        match piece.clone() {
            parse::Piece::Lit(s) => {
                unfinished_literal.push_str(s);
            }
            parse::Piece::NextArgument(parse::Argument { position, position_span, format }) => {
                if !unfinished_literal.is_empty() {
                    template.push(FormatArgsPiece::Literal(Symbol::intern(&unfinished_literal)));
                    unfinished_literal.clear();
                }

                let span =
                    parser.arg_places.get(placeholder_index).and_then(|s| to_span(s.clone()));
                placeholder_index += 1;

                let position_span = to_span(position_span);
                let argument = match position {
                    parse::ArgumentImplicitlyIs(i) => lookup_arg(
                        Index(i),
                        position_span,
                        Placeholder(span),
                        FormatArgPositionKind::Implicit,
                    ),
                    parse::ArgumentIs(i) => lookup_arg(
                        Index(i),
                        position_span,
                        Placeholder(span),
                        FormatArgPositionKind::Number,
                    ),
                    parse::ArgumentNamed(name) => lookup_arg(
                        Name(name, position_span),
                        position_span,
                        Placeholder(span),
                        FormatArgPositionKind::Named,
                    ),
                };

                let alignment = match format.align {
                    parse::AlignUnknown => None,
                    parse::AlignLeft => Some(FormatAlignment::Left),
                    parse::AlignRight => Some(FormatAlignment::Right),
                    parse::AlignCenter => Some(FormatAlignment::Center),
                };

                let format_trait = match format.ty {
                    "" => FormatTrait::Display,
                    "?" => FormatTrait::Debug,
                    "e" => FormatTrait::LowerExp,
                    "E" => FormatTrait::UpperExp,
                    "o" => FormatTrait::Octal,
                    "p" => FormatTrait::Pointer,
                    "b" => FormatTrait::Binary,
                    "x" => FormatTrait::LowerHex,
                    "X" => FormatTrait::UpperHex,
                    _ => {
                        invalid_placeholder_type_error(ecx, format.ty, format.ty_span, fmt_span);
                        FormatTrait::Display
                    }
                };

                let precision_span = format.precision_span.and_then(to_span);
                let precision = match format.precision {
                    parse::CountIs(n) => Some(FormatCount::Literal(n)),
                    parse::CountIsName(name, name_span) => Some(FormatCount::Argument(lookup_arg(
                        Name(name, to_span(name_span)),
                        precision_span,
                        Precision,
                        FormatArgPositionKind::Named,
                    ))),
                    parse::CountIsParam(i) => Some(FormatCount::Argument(lookup_arg(
                        Index(i),
                        precision_span,
                        Precision,
                        FormatArgPositionKind::Number,
                    ))),
                    parse::CountIsStar(i) => Some(FormatCount::Argument(lookup_arg(
                        Index(i),
                        precision_span,
                        Precision,
                        FormatArgPositionKind::Implicit,
                    ))),
                    parse::CountImplied => None,
                };

                let width_span = format.width_span.and_then(to_span);
                let width = match format.width {
                    parse::CountIs(n) => Some(FormatCount::Literal(n)),
                    parse::CountIsName(name, name_span) => Some(FormatCount::Argument(lookup_arg(
                        Name(name, to_span(name_span)),
                        width_span,
                        Width,
                        FormatArgPositionKind::Named,
                    ))),
                    parse::CountIsParam(i) => Some(FormatCount::Argument(lookup_arg(
                        Index(i),
                        width_span,
                        Width,
                        FormatArgPositionKind::Number,
                    ))),
                    parse::CountIsStar(_) => unreachable!(),
                    parse::CountImplied => None,
                };

                template.push(FormatArgsPiece::Placeholder(FormatPlaceholder {
                    argument,
                    span,
                    format_trait,
                    format_options: FormatOptions {
                        fill: format.fill,
                        alignment,
                        sign: format.sign.map(|s| match s {
                            parse::Sign::Plus => FormatSign::Plus,
                            parse::Sign::Minus => FormatSign::Minus,
                        }),
                        alternate: format.alternate,
                        zero_pad: format.zero_pad,
                        debug_hex: format.debug_hex.map(|s| match s {
                            parse::DebugHex::Lower => FormatDebugHex::Lower,
                            parse::DebugHex::Upper => FormatDebugHex::Upper,
                        }),
                        precision,
                        width,
                    },
                }));
            }
        }
    }

    if !unfinished_literal.is_empty() {
        template.push(FormatArgsPiece::Literal(Symbol::intern(&unfinished_literal)));
    }

    if !invalid_refs.is_empty() {
        report_invalid_references(ecx, &invalid_refs, &template, fmt_span, &args, parser);
    }

    let unused = used
        .iter()
        .enumerate()
        .filter(|&(_, used)| !used)
        .map(|(i, _)| {
            let named = matches!(args.explicit_args()[i].kind, FormatArgumentKind::Named(_));
            (args.explicit_args()[i].expr.span, named)
        })
        .collect::<Vec<_>>();

    let has_unused = !unused.is_empty();
    if has_unused {
        // If there's a lot of unused arguments,
        // let's check if this format arguments looks like another syntax (printf / shell).
        let detect_foreign_fmt = unused.len() > args.explicit_args().len() / 2;
        report_missing_placeholders(
            ecx,
            unused,
            &used,
            &args,
            &pieces,
            &invalid_refs,
            detect_foreign_fmt,
            str_style,
            fmt_str,
            uncooked_fmt_str.1.as_str(),
            fmt_span,
        );
    }

    // Only check for unused named argument names if there are no other errors to avoid causing
    // too much noise in output errors, such as when a named argument is entirely unused.
    if invalid_refs.is_empty() && !has_unused && !unnamed_arg_after_named_arg {
        for &(index, span, used_as) in &numeric_references_to_named_arg {
            let (position_sp_to_replace, position_sp_for_msg) = match used_as {
                Placeholder(pspan) => (span, pspan),
                Precision => {
                    // Strip the leading `.` for precision.
                    let span = span.map(|span| span.with_lo(span.lo() + BytePos(1)));
                    (span, span)
                }
                Width => (span, span),
            };
            let arg_name = args.explicit_args()[index].kind.ident().unwrap();
            ecx.buffered_early_lint.push(BufferedEarlyLint {
                span: Some(arg_name.span.into()),
                node_id: rustc_ast::CRATE_NODE_ID,
                lint_id: LintId::of(NAMED_ARGUMENTS_USED_POSITIONALLY),
                diagnostic: DecorateDiagCompat(Box::new(move |dcx, level, sess| {
                    let (suggestion, name) =
                        if let Some(positional_arg_to_replace) = position_sp_to_replace {
                            let mut name = arg_name.name.to_string();
                            let is_formatting_arg = matches!(used_as, Width | Precision);
                            if is_formatting_arg {
                                name.push('$')
                            };
                            let span_to_replace = if let Ok(positional_arg_content) = sess
                                .downcast_ref::<rustc_session::Session>()
                                .expect("expected a `Session`")
                                .source_map()
                                .span_to_snippet(positional_arg_to_replace)
                                && positional_arg_content.starts_with(':')
                            {
                                positional_arg_to_replace.shrink_to_lo()
                            } else {
                                positional_arg_to_replace
                            };
                            (Some(span_to_replace), name)
                        } else {
                            (None, String::new())
                        };

                    errors::NamedArgumentUsedPositionally {
                        named_arg_sp: arg_name.span,
                        position_label_sp: position_sp_for_msg,
                        suggestion,
                        name,
                        named_arg_name: arg_name.name.to_string(),
                    }
                    .into_diag(dcx, level)
                })),
            });
        }
    }

    ExpandResult::Ready(Ok(FormatArgs {
        span: fmt_span,
        template,
        arguments: args,
        uncooked_fmt_str,
        is_source_literal,
    }))
}

fn invalid_placeholder_type_error(
    ecx: &ExtCtxt<'_>,
    ty: &str,
    ty_span: Option<Range<usize>>,
    fmt_span: Span,
) {
    let sp = ty_span.map(|sp| fmt_span.from_inner(InnerSpan::new(sp.start, sp.end)));
    let suggs = if let Some(sp) = sp {
        [
            ("", "Display"),
            ("?", "Debug"),
            ("e", "LowerExp"),
            ("E", "UpperExp"),
            ("o", "Octal"),
            ("p", "Pointer"),
            ("b", "Binary"),
            ("x", "LowerHex"),
            ("X", "UpperHex"),
        ]
        .into_iter()
        .map(|(fmt, trait_name)| errors::FormatUnknownTraitSugg { span: sp, fmt, trait_name })
        .collect()
    } else {
        vec![]
    };
    ecx.dcx().emit_err(errors::FormatUnknownTrait { span: sp.unwrap_or(fmt_span), ty, suggs });
}

fn report_missing_placeholders(
    ecx: &ExtCtxt<'_>,
    unused: Vec<(Span, bool)>,
    used: &[bool],
    args: &FormatArguments,
    pieces: &[parse::Piece<'_>],
    invalid_refs: &[(usize, Option<Span>, PositionUsedAs, FormatArgPositionKind)],
    detect_foreign_fmt: bool,
    str_style: Option<usize>,
    fmt_str: &str,
    uncooked_fmt_str: &str,
    fmt_span: Span,
) {
    let mut diag = if let &[(span, named)] = &unused[..] {
        ecx.dcx().create_err(errors::FormatUnusedArg { span, named })
    } else {
        let unused_labels =
            unused.iter().map(|&(span, named)| errors::FormatUnusedArg { span, named }).collect();
        let unused_spans = unused.iter().map(|&(span, _)| span).collect();
        ecx.dcx().create_err(errors::FormatUnusedArgs {
            fmt: fmt_span,
            unused: unused_spans,
            unused_labels,
        })
    };

    let placeholders = pieces
        .iter()
        .filter_map(|piece| {
            if let parse::Piece::NextArgument(argument) = piece
                && let ArgumentNamed(binding) = argument.position
            {
                let span = fmt_span.from_inner(InnerSpan::new(
                    argument.position_span.start,
                    argument.position_span.end,
                ));
                Some((span, binding))
            } else {
                None
            }
        })
        .collect::<Vec<_>>();

    if !placeholders.is_empty() {
        if let Some(new_diag) = report_redundant_format_arguments(ecx, args, used, placeholders) {
            diag.cancel();
            new_diag.emit();
            return;
        }
    }

    // Used to ensure we only report translations for *one* kind of foreign format.
    let mut found_foreign = false;

    // Decide if we want to look for foreign formatting directives.
    if detect_foreign_fmt {
        use super::format_foreign as foreign;

        // The set of foreign substitutions we've explained. This prevents spamming the user
        // with `%d should be written as {}` over and over again.
        let mut explained = FxHashSet::default();

        macro_rules! check_foreign {
            ($kind:ident) => {{
                let mut show_doc_note = false;

                let mut suggestions = vec![];
                // account for `"` and account for raw strings `r#`
                let padding = str_style.map(|i| i + 2).unwrap_or(1);
                for sub in foreign::$kind::iter_subs(fmt_str, padding) {
                    let (trn, success) = match sub.translate() {
                        Ok(trn) => (trn, true),
                        Err(Some(msg)) => (msg, false),

                        // If it has no translation, don't call it out specifically.
                        _ => continue,
                    };

                    let pos = sub.position();
                    if !explained.insert(sub.to_string()) {
                        continue;
                    }

                    if !found_foreign {
                        found_foreign = true;
                        show_doc_note = true;
                    }

                    let sp = fmt_span.from_inner(pos);

                    if success {
                        suggestions.push((sp, trn));
                    } else {
                        diag.span_note(
                            sp,
                            format!("format specifiers use curly braces, and {}", trn),
                        );
                    }
                }

                if show_doc_note {
                    diag.note(concat!(
                        stringify!($kind),
                        " formatting is not supported; see the documentation for `std::fmt`",
                    ));
                }
                if suggestions.len() > 0 {
                    diag.multipart_suggestion(
                        "format specifiers use curly braces",
                        suggestions,
                        Applicability::MachineApplicable,
                    );
                }
            }};
        }

        check_foreign!(printf);
        if !found_foreign {
            check_foreign!(shell);
        }
    }
    if !found_foreign && unused.len() == 1 {
        diag.span_label(fmt_span, "formatting specifier missing");
    }

    if !found_foreign && invalid_refs.is_empty() {
        // Show example if user didn't use any format specifiers
        let show_example = !used.contains(&true);

        if !show_example {
            if unused.len() > 1 {
                diag.note(format!("consider adding {} format specifiers", unused.len()));
            }
        } else {
            let msg = if unused.len() == 1 {
                "a format specifier".to_string()
            } else {
                format!("{} format specifiers", unused.len())
            };

            let sugg = match str_style {
                None => format!("\"{}{}\"", uncooked_fmt_str, "{}".repeat(unused.len())),
                Some(n_hashes) => format!(
                    "r{hashes}\"{uncooked_fmt_str}{fmt_specifiers}\"{hashes}",
                    hashes = "#".repeat(n_hashes),
                    fmt_specifiers = "{}".repeat(unused.len())
                ),
            };
            let msg = format!("format specifiers use curly braces, consider adding {msg}");

            diag.span_suggestion_verbose(fmt_span, msg, sugg, Applicability::MaybeIncorrect);
        }
    }

    diag.emit();
}

/// This function detects and reports unused format!() arguments that are
/// redundant due to implicit captures (e.g. `format!("{x}", x)`).
fn report_redundant_format_arguments<'a>(
    ecx: &ExtCtxt<'a>,
    args: &FormatArguments,
    used: &[bool],
    placeholders: Vec<(Span, &str)>,
) -> Option<Diag<'a>> {
    let mut fmt_arg_indices = vec![];
    let mut args_spans = vec![];
    let mut fmt_spans = vec![];

    for (i, unnamed_arg) in args.unnamed_args().iter().enumerate().rev() {
        let Some(ty) = unnamed_arg.expr.to_ty() else { continue };
        let Some(argument_binding) = ty.kind.is_simple_path() else { continue };
        let argument_binding = argument_binding.as_str();

        if used[i] {
            continue;
        }

        let matching_placeholders = placeholders
            .iter()
            .filter(|(_, inline_binding)| argument_binding == *inline_binding)
            .map(|(span, _)| span)
            .collect::<Vec<_>>();

        if !matching_placeholders.is_empty() {
            fmt_arg_indices.push(i);
            args_spans.push(unnamed_arg.expr.span);
            for span in &matching_placeholders {
                if fmt_spans.contains(*span) {
                    continue;
                }
                fmt_spans.push(**span);
            }
        }
    }

    if !args_spans.is_empty() {
        let multispan = MultiSpan::from(fmt_spans);
        let mut suggestion_spans = vec![];

        for (arg_span, fmt_arg_idx) in args_spans.iter().zip(fmt_arg_indices.iter()) {
            let span = if fmt_arg_idx + 1 == args.explicit_args().len() {
                *arg_span
            } else {
                arg_span.until(args.explicit_args()[*fmt_arg_idx + 1].expr.span)
            };

            suggestion_spans.push(span);
        }

        let sugg = if args.named_args().len() == 0 {
            Some(errors::FormatRedundantArgsSugg { spans: suggestion_spans })
        } else {
            None
        };

        return Some(ecx.dcx().create_err(errors::FormatRedundantArgs {
            n: args_spans.len(),
            span: MultiSpan::from(args_spans),
            note: multispan,
            sugg,
        }));
    }

    None
}

/// Handle invalid references to positional arguments. Output different
/// errors for the case where all arguments are positional and for when
/// there are named arguments or numbered positional arguments in the
/// format string.
fn report_invalid_references(
    ecx: &ExtCtxt<'_>,
    invalid_refs: &[(usize, Option<Span>, PositionUsedAs, FormatArgPositionKind)],
    template: &[FormatArgsPiece],
    fmt_span: Span,
    args: &FormatArguments,
    parser: parse::Parser<'_>,
) {
    let num_args_desc = match args.explicit_args().len() {
        0 => "no arguments were given".to_string(),
        1 => "there is 1 argument".to_string(),
        n => format!("there are {n} arguments"),
    };

    let mut e;

    if template.iter().all(|piece| match piece {
        FormatArgsPiece::Placeholder(FormatPlaceholder {
            argument: FormatArgPosition { kind: FormatArgPositionKind::Number, .. },
            ..
        }) => false,
        FormatArgsPiece::Placeholder(FormatPlaceholder {
            format_options:
                FormatOptions {
                    precision:
                        Some(FormatCount::Argument(FormatArgPosition {
                            kind: FormatArgPositionKind::Number,
                            ..
                        })),
                    ..
                }
                | FormatOptions {
                    width:
                        Some(FormatCount::Argument(FormatArgPosition {
                            kind: FormatArgPositionKind::Number,
                            ..
                        })),
                    ..
                },
            ..
        }) => false,
        _ => true,
    }) {
        // There are no numeric positions.
        // Collect all the implicit positions:
        let mut spans = Vec::new();
        let mut num_placeholders = 0;
        for piece in template {
            let mut placeholder = None;
            // `{arg:.*}`
            if let FormatArgsPiece::Placeholder(FormatPlaceholder {
                format_options:
                    FormatOptions {
                        precision:
                            Some(FormatCount::Argument(FormatArgPosition {
                                span,
                                kind: FormatArgPositionKind::Implicit,
                                ..
                            })),
                        ..
                    },
                ..
            }) = piece
            {
                placeholder = *span;
                num_placeholders += 1;
            }
            // `{}`
            if let FormatArgsPiece::Placeholder(FormatPlaceholder {
                argument: FormatArgPosition { kind: FormatArgPositionKind::Implicit, .. },
                span,
                ..
            }) = piece
            {
                placeholder = *span;
                num_placeholders += 1;
            }
            // For `{:.*}`, we only push one span.
            spans.extend(placeholder);
        }
        let span = if spans.is_empty() {
            MultiSpan::from_span(fmt_span)
        } else {
            MultiSpan::from_spans(spans)
        };
        e = ecx.dcx().create_err(errors::FormatPositionalMismatch {
            span,
            n: num_placeholders,
            desc: num_args_desc,
            highlight: SingleLabelManySpans {
                spans: args.explicit_args().iter().map(|arg| arg.expr.span).collect(),
                label: "",
            },
        });
        // Point out `{:.*}` placeholders: those take an extra argument.
        let mut has_precision_star = false;
        for piece in template {
            if let FormatArgsPiece::Placeholder(FormatPlaceholder {
                format_options:
                    FormatOptions {
                        precision:
                            Some(FormatCount::Argument(FormatArgPosition {
                                index,
                                span: Some(span),
                                kind: FormatArgPositionKind::Implicit,
                                ..
                            })),
                        ..
                    },
                ..
            }) = piece
            {
                let (Ok(index) | Err(index)) = index;
                has_precision_star = true;
                e.span_label(
                    *span,
                    format!(
                        "this precision flag adds an extra required argument at position {}, which is why there {} expected",
                        index,
                        if num_placeholders == 1 {
                            "is 1 argument".to_string()
                        } else {
                            format!("are {num_placeholders} arguments")
                        },
                    ),
                );
            }
        }
        if has_precision_star {
            e.note("positional arguments are zero-based");
        }
    } else {
        let mut indexes: Vec<_> = invalid_refs.iter().map(|&(index, _, _, _)| index).collect();
        // Avoid `invalid reference to positional arguments 7 and 7 (there is 1 argument)`
        // for `println!("{7:7$}", 1);`
        indexes.sort();
        indexes.dedup();
        let span: MultiSpan = if !parser.is_source_literal || parser.arg_places.is_empty() {
            MultiSpan::from_span(fmt_span)
        } else {
            MultiSpan::from_spans(invalid_refs.iter().filter_map(|&(_, span, _, _)| span).collect())
        };
        let arg_list = format!(
            "argument{} {}",
            pluralize!(indexes.len()),
            listify(&indexes, |i: &usize| i.to_string()).unwrap_or_default()
        );
        e = ecx.dcx().struct_span_err(
            span,
            format!("invalid reference to positional {arg_list} ({num_args_desc})"),
        );
        e.note("positional arguments are zero-based");
    }

    if template.iter().any(|piece| match piece {
        FormatArgsPiece::Placeholder(FormatPlaceholder { format_options: f, .. }) => {
            *f != FormatOptions::default()
        }
        _ => false,
    }) {
        e.note("for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html");
    }

    e.emit();
}

fn expand_format_args_impl<'cx>(
    ecx: &'cx mut ExtCtxt<'_>,
    mut sp: Span,
    tts: TokenStream,
    nl: bool,
) -> MacroExpanderResult<'cx> {
    sp = ecx.with_def_site_ctxt(sp);
    ExpandResult::Ready(match parse_args(ecx, sp, tts) {
        Ok(input) => {
            let ExpandResult::Ready(mac) = make_format_args(ecx, input, nl, sp) else {
                return ExpandResult::Retry(());
            };
            match mac {
                Ok(format_args) => {
                    MacEager::expr(ecx.expr(sp, ExprKind::FormatArgs(Box::new(format_args))))
                }
                Err(guar) => MacEager::expr(DummyResult::raw_expr(sp, Some(guar))),
            }
        }
        Err(err) => {
            let guar = err.emit();
            DummyResult::any(sp, guar)
        }
    })
}

pub(crate) fn expand_format_args<'cx>(
    ecx: &'cx mut ExtCtxt<'_>,
    sp: Span,
    tts: TokenStream,
) -> MacroExpanderResult<'cx> {
    expand_format_args_impl(ecx, sp, tts, false)
}

pub(crate) fn expand_format_args_nl<'cx>(
    ecx: &'cx mut ExtCtxt<'_>,
    sp: Span,
    tts: TokenStream,
) -> MacroExpanderResult<'cx> {
    expand_format_args_impl(ecx, sp, tts, true)
}
