//! Utilities for rendering escape sequence errors as diagnostics.

use std::iter::once;
use std::ops::Range;

use rustc_errors::{Applicability, Diag, DiagCtxtHandle, ErrorGuaranteed};
use rustc_literal_escaper::{EscapeError, Mode};
use rustc_span::{BytePos, Span};
use tracing::debug;

use crate::errors::{MoreThanOneCharNote, MoreThanOneCharSugg, NoBraceUnicodeSub, UnescapeError};

pub(crate) fn emit_unescape_error(
    dcx: DiagCtxtHandle<'_>,
    // interior part of the literal, between quotes
    lit: &str,
    // full span of the literal, including quotes and any prefix
    full_lit_span: Span,
    // span of the error part of the literal
    err_span: Span,
    mode: Mode,
    // range of the error inside `lit`
    range: Range<usize>,
    error: EscapeError,
) -> Option<ErrorGuaranteed> {
    debug!(
        "emit_unescape_error: {:?}, {:?}, {:?}, {:?}, {:?}",
        lit, full_lit_span, mode, range, error
    );
    let last_char = || {
        let c = lit[range.clone()].chars().next_back().unwrap();
        let span = err_span.with_lo(err_span.hi() - BytePos(c.len_utf8() as u32));
        (c, span)
    };
    Some(match error {
        EscapeError::LoneSurrogateUnicodeEscape => {
            dcx.emit_err(UnescapeError::InvalidUnicodeEscape { span: err_span, surrogate: true })
        }
        EscapeError::OutOfRangeUnicodeEscape => {
            dcx.emit_err(UnescapeError::InvalidUnicodeEscape { span: err_span, surrogate: false })
        }
        EscapeError::MoreThanOneChar => {
            use unicode_normalization::UnicodeNormalization;
            use unicode_normalization::char::is_combining_mark;
            let mut sugg = None;
            let mut note = None;

            let lit_chars = lit.chars().collect::<Vec<_>>();
            let (first, rest) = lit_chars.split_first().unwrap();
            if rest.iter().copied().all(is_combining_mark) {
                let normalized = lit.nfc().to_string();
                if normalized.chars().count() == 1 {
                    let ch = normalized.chars().next().unwrap().escape_default().to_string();
                    sugg = Some(MoreThanOneCharSugg::NormalizedForm {
                        span: err_span,
                        ch,
                        normalized,
                    });
                }
                let escaped_marks =
                    rest.iter().map(|c| c.escape_default().to_string()).collect::<Vec<_>>();
                note = Some(MoreThanOneCharNote::AllCombining {
                    span: err_span,
                    chr: format!("{first}"),
                    len: escaped_marks.len(),
                    escaped_marks: escaped_marks.join(""),
                });
            } else {
                let printable: Vec<char> = lit
                    .chars()
                    .filter(|&x| {
                        unicode_width::UnicodeWidthChar::width(x).unwrap_or(0) != 0
                            && !x.is_whitespace()
                    })
                    .collect();

                if let &[ch] = printable.as_slice() {
                    sugg = Some(MoreThanOneCharSugg::RemoveNonPrinting {
                        span: err_span,
                        ch: ch.to_string(),
                    });
                    note = Some(MoreThanOneCharNote::NonPrinting {
                        span: err_span,
                        escaped: lit.escape_default().to_string(),
                    });
                }
            };
            let sugg = sugg.unwrap_or_else(|| {
                let prefix = mode.prefix_noraw();
                let mut escaped = String::with_capacity(lit.len());
                let mut in_escape = false;
                for c in lit.chars() {
                    match c {
                        '\\' => in_escape = !in_escape,
                        '"' if !in_escape => escaped.push('\\'),
                        _ => in_escape = false,
                    }
                    escaped.push(c);
                }
                if escaped.len() != lit.len() || full_lit_span.is_empty() {
                    let sugg = format!("{prefix}\"{escaped}\"");
                    MoreThanOneCharSugg::QuotesFull {
                        span: full_lit_span,
                        is_byte: mode == Mode::Byte,
                        sugg,
                    }
                } else {
                    MoreThanOneCharSugg::Quotes {
                        start: full_lit_span
                            .with_hi(full_lit_span.lo() + BytePos((prefix.len() + 1) as u32)),
                        end: full_lit_span.with_lo(full_lit_span.hi() - BytePos(1)),
                        is_byte: mode == Mode::Byte,
                        prefix,
                    }
                }
            });
            dcx.emit_err(UnescapeError::MoreThanOneChar {
                span: full_lit_span,
                note,
                suggestion: sugg,
            })
        }
        EscapeError::EscapeOnlyChar => {
            let (c, char_span) = last_char();
            dcx.emit_err(UnescapeError::EscapeOnlyChar {
                span: err_span,
                char_span,
                escaped_sugg: c.escape_default().to_string(),
                escaped_msg: escaped_char(c),
                byte: mode == Mode::Byte,
            })
        }
        EscapeError::BareCarriageReturn => {
            let double_quotes = mode.in_double_quotes();
            dcx.emit_err(UnescapeError::BareCr { span: err_span, double_quotes })
        }
        EscapeError::BareCarriageReturnInRawString => {
            assert!(mode.in_double_quotes());
            dcx.emit_err(UnescapeError::BareCrRawString(err_span))
        }
        EscapeError::InvalidEscape => {
            let (c, span) = last_char();

            let label = if mode == Mode::Byte || mode == Mode::ByteStr {
                "unknown byte escape"
            } else {
                "unknown character escape"
            };
            let ec = escaped_char(c);
            let mut diag = dcx.struct_span_err(span, format!("{label}: `{ec}`"));
            diag.span_label(span, label);
            if c == '{' || c == '}' && matches!(mode, Mode::Str | Mode::RawStr) {
                diag.help(
                    "if used in a formatting string, curly braces are escaped with `{{` and `}}`",
                );
            } else if c == '\r' {
                diag.help(
                    "this is an isolated carriage return; consider checking your editor and \
                     version control settings",
                );
            } else {
                if mode == Mode::Str || mode == Mode::Char {
                    diag.span_suggestion(
                        full_lit_span,
                        "if you meant to write a literal backslash (perhaps escaping in a regular expression), consider a raw string literal",
                        format!("r\"{lit}\""),
                        Applicability::MaybeIncorrect,
                    );
                }

                diag.help(
                    "for more information, visit \
                     <https://doc.rust-lang.org/reference/tokens.html#literals>",
                );

                foreign_escape_suggestion(&mut diag, (&ec, span), err_span);
            }
            diag.emit()
        }
        EscapeError::TooShortHexEscape => dcx.emit_err(UnescapeError::TooShortHexEscape(err_span)),
        EscapeError::InvalidCharInHexEscape | EscapeError::InvalidCharInUnicodeEscape => {
            let (c, span) = last_char();
            let is_hex = error == EscapeError::InvalidCharInHexEscape;
            let ch = escaped_char(c);
            dcx.emit_err(UnescapeError::InvalidCharInEscape { span, is_hex, ch })
        }
        EscapeError::NonAsciiCharInByte => {
            let (c, span) = last_char();
            let desc = match mode {
                Mode::Byte => "byte literal",
                Mode::ByteStr => "byte string literal",
                Mode::RawByteStr => "raw byte string literal",
                _ => panic!("non-is_byte literal paired with NonAsciiCharInByte"),
            };
            let mut err = dcx.struct_span_err(span, format!("non-ASCII character in {desc}"));
            let postfix = if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
                format!(" but is {c:?}")
            } else {
                String::new()
            };
            err.span_label(span, format!("must be ASCII{postfix}"));
            // Note: the \\xHH suggestions are not given for raw byte string
            // literals, because they are araw and so cannot use any escapes.
            if (c as u32) <= 0xFF && mode != Mode::RawByteStr {
                err.span_suggestion(
                    span,
                    format!(
                        "if you meant to use the unicode code point for {c:?}, use a \\xHH escape"
                    ),
                    format!("\\x{:X}", c as u32),
                    Applicability::MaybeIncorrect,
                );
            } else if mode == Mode::Byte {
                err.span_label(span, "this multibyte character does not fit into a single byte");
            } else if mode != Mode::RawByteStr {
                let mut utf8 = String::new();
                utf8.push(c);
                err.span_suggestion(
                    span,
                    format!("if you meant to use the UTF-8 encoding of {c:?}, use \\xHH escapes"),
                    utf8.as_bytes()
                        .iter()
                        .map(|b: &u8| format!("\\x{:X}", *b))
                        .fold("".to_string(), |a, c| a + &c),
                    Applicability::MaybeIncorrect,
                );
            }
            err.emit()
        }
        EscapeError::OutOfRangeHexEscape => {
            let mut err = dcx.struct_span_err(err_span, "out of range hex escape");
            err.span_label(err_span, "must be a character in the range [\\x00-\\x7f]");

            let escape_str = &lit[range];
            if lit.len() <= 4
                && escape_str.len() == 4
                && escape_str.starts_with("\\x")
                && let Ok(value) = u8::from_str_radix(&escape_str[2..4], 16)
                && matches!(mode, Mode::Char | Mode::Str)
            {
                err.help(format!("if you want to write a byte literal, use `b'{}'`", escape_str));
                err.help(format!(
                    "if you want to write a Unicode character, use `'\\u{{{:X}}}'`",
                    value
                ));
            }

            err.emit()
        }
        EscapeError::LeadingUnderscoreUnicodeEscape => {
            let (c, span) = last_char();
            dcx.emit_err(UnescapeError::LeadingUnderscoreUnicodeEscape {
                span,
                ch: escaped_char(c),
            })
        }
        EscapeError::OverlongUnicodeEscape => {
            dcx.emit_err(UnescapeError::OverlongUnicodeEscape(err_span))
        }
        EscapeError::UnclosedUnicodeEscape => {
            dcx.emit_err(UnescapeError::UnclosedUnicodeEscape(err_span, err_span.shrink_to_hi()))
        }
        EscapeError::NoBraceInUnicodeEscape => {
            let mut suggestion = "\\u{".to_owned();
            let mut suggestion_len = 0;
            let (c, char_span) = last_char();
            let chars = once(c).chain(lit[range.end..].chars());
            for c in chars.take(6).take_while(|c| c.is_digit(16)) {
                suggestion.push(c);
                suggestion_len += c.len_utf8();
            }

            let (label, sub) = if suggestion_len > 0 {
                suggestion.push('}');
                let hi = char_span.lo() + BytePos(suggestion_len as u32);
                (None, NoBraceUnicodeSub::Suggestion { span: err_span.with_hi(hi), suggestion })
            } else {
                (Some(err_span), NoBraceUnicodeSub::Help)
            };
            dcx.emit_err(UnescapeError::NoBraceInUnicodeEscape { span: err_span, label, sub })
        }
        EscapeError::UnicodeEscapeInByte => {
            dcx.emit_err(UnescapeError::UnicodeEscapeInByte(err_span))
        }
        EscapeError::EmptyUnicodeEscape => {
            dcx.emit_err(UnescapeError::EmptyUnicodeEscape(err_span))
        }
        EscapeError::ZeroChars => dcx.emit_err(UnescapeError::ZeroChars(err_span)),
        EscapeError::LoneSlash => dcx.emit_err(UnescapeError::LoneSlash(err_span)),
        EscapeError::NulInCStr => dcx.emit_err(UnescapeError::NulInCStr { span: err_span }),
        EscapeError::UnskippedWhitespaceWarning => {
            let (c, char_span) = last_char();
            dcx.emit_warn(UnescapeError::UnskippedWhitespace {
                span: err_span,
                ch: escaped_char(c),
                char_span,
            });
            return None;
        }
        EscapeError::MultipleSkippedLinesWarning => {
            dcx.emit_warn(UnescapeError::MultipleSkippedLinesWarning(err_span));
            return None;
        }
    })
}

/// Add additional suggestions for escapes that are supported by C.
fn foreign_escape_suggestion(
    diag: &mut Diag<'_>,
    (escaped_char, escape_span): (&str, Span),
    err_span: Span,
) {
    if escaped_char == "?" {
        diag.span_suggestion(
            err_span,
            "if you meant to write a literal question mark, don't escape the character",
            "?",
            Applicability::MaybeIncorrect,
        );
        return;
    }

    if u8::from_str_radix(escaped_char, 8).is_ok() {
        diag.help(r"if you meant to write an ASCII control code, use a `\xNN` hex escape");
        return;
    }

    let (name, hex) = match escaped_char {
        "a" => ("an audible bell", "07"),
        "b" => ("a backspace", "08"),
        "f" => ("a form feed", "0C"),
        "v" => ("a vertical tab", "0B"),
        "e" => ("an ANSI escape sequence", "1B"),
        _ => return,
    };

    diag.span_suggestion(
        escape_span,
        format!("if you meant to write {name}, use a hex escape"),
        format!("x{hex}"),
        Applicability::MaybeIncorrect,
    );
}

/// Pushes a character to a message string for error reporting
pub(crate) fn escaped_char(c: char) -> String {
    match c {
        '\u{20}'..='\u{7e}' => {
            // Don't escape \, ' or " for user-facing messages
            c.to_string()
        }
        _ => c.escape_default().to_string(),
    }
}
