use crate::{abort_now, check_correctness, sealed::Sealed, SpanRange};
use proc_macro2::Span;
use proc_macro2::TokenStream;

use quote::{quote_spanned, ToTokens};

/// Represents a diagnostic level
///
/// # Warnings
///
/// Warnings are ignored on stable/beta
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum Level {
    Error,
    Warning,
}

/// Represents a single diagnostic message
#[derive(Debug)]
#[must_use = "A diagnostic does nothing unless emitted"]
pub struct Diagnostic {
    pub(crate) level: Level,
    pub(crate) span_range: SpanRange,
    pub(crate) msg: String,
    pub(crate) suggestions: Vec<(SuggestionKind, String, Option<SpanRange>)>,
    pub(crate) children: Vec<(SpanRange, String)>,
}

/// A collection of methods that do not exist in `proc_macro::Diagnostic`
/// but still useful to have around.
///
/// This trait is sealed and cannot be implemented outside of `proc_macro_error`.
pub trait DiagnosticExt: Sealed {
    /// Create a new diagnostic message that points to the `span_range`.
    ///
    /// This function is the same as `Diagnostic::spanned` but produces considerably
    /// better error messages for multi-token spans on stable.
    fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self;

    /// Add another error message to self such that it will be emitted right after
    /// the main message.
    ///
    /// This function is the same as `Diagnostic::span_error` but produces considerably
    /// better error messages for multi-token spans on stable.
    #[must_use]
    fn span_range_error(self, span_range: SpanRange, msg: String) -> Self;

    /// Attach a "help" note to your main message, the note will have it's own span on nightly.
    ///
    /// This function is the same as `Diagnostic::span_help` but produces considerably
    /// better error messages for multi-token spans on stable.
    ///
    /// # Span
    ///
    /// The span is ignored on stable, the note effectively inherits its parent's (main message) span
    #[must_use]
    fn span_range_help(self, span_range: SpanRange, msg: String) -> Self;

    /// Attach a note to your main message, the note will have it's own span on nightly.
    ///
    /// This function is the same as `Diagnostic::span_note` but produces considerably
    /// better error messages for multi-token spans on stable.
    ///
    /// # Span
    ///
    /// The span is ignored on stable, the note effectively inherits its parent's (main message) span
    #[must_use]
    fn span_range_note(self, span_range: SpanRange, msg: String) -> Self;
}

impl DiagnosticExt for Diagnostic {
    fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self {
        Diagnostic {
            level,
            span_range,
            msg: message,
            suggestions: vec![],
            children: vec![],
        }
    }

    fn span_range_error(mut self, span_range: SpanRange, msg: String) -> Self {
        self.children.push((span_range, msg));
        self
    }

    fn span_range_help(mut self, span_range: SpanRange, msg: String) -> Self {
        self.suggestions
            .push((SuggestionKind::Help, msg, Some(span_range)));
        self
    }

    fn span_range_note(mut self, span_range: SpanRange, msg: String) -> Self {
        self.suggestions
            .push((SuggestionKind::Note, msg, Some(span_range)));
        self
    }
}

impl Diagnostic {
    /// Create a new diagnostic message that points to `Span::call_site()`
    pub fn new(level: Level, message: String) -> Self {
        Diagnostic::spanned(Span::call_site(), level, message)
    }

    /// Create a new diagnostic message that points to the `span`
    pub fn spanned(span: Span, level: Level, message: String) -> Self {
        Diagnostic::spanned_range(
            SpanRange {
                first: span,
                last: span,
            },
            level,
            message,
        )
    }

    /// Add another error message to self such that it will be emitted right after
    /// the main message.
    pub fn span_error(self, span: Span, msg: String) -> Self {
        self.span_range_error(
            SpanRange {
                first: span,
                last: span,
            },
            msg,
        )
    }

    /// Attach a "help" note to your main message, the note will have it's own span on nightly.
    ///
    /// # Span
    ///
    /// The span is ignored on stable, the note effectively inherits its parent's (main message) span
    pub fn span_help(self, span: Span, msg: String) -> Self {
        self.span_range_help(
            SpanRange {
                first: span,
                last: span,
            },
            msg,
        )
    }

    /// Attach a "help" note to your main message.
    pub fn help(mut self, msg: String) -> Self {
        self.suggestions.push((SuggestionKind::Help, msg, None));
        self
    }

    /// Attach a note to your main message, the note will have it's own span on nightly.
    ///
    /// # Span
    ///
    /// The span is ignored on stable, the note effectively inherits its parent's (main message) span
    pub fn span_note(self, span: Span, msg: String) -> Self {
        self.span_range_note(
            SpanRange {
                first: span,
                last: span,
            },
            msg,
        )
    }

    /// Attach a note to your main message
    pub fn note(mut self, msg: String) -> Self {
        self.suggestions.push((SuggestionKind::Note, msg, None));
        self
    }

    /// The message of main warning/error (no notes attached)
    #[must_use]
    pub fn message(&self) -> &str {
        &self.msg
    }

    /// Abort the proc-macro's execution and display the diagnostic.
    ///
    /// # Warnings
    ///
    /// Warnings are not emitted on stable and beta, but this function will abort anyway.
    pub fn abort(self) -> ! {
        self.emit();
        abort_now()
    }

    /// Display the diagnostic while not aborting macro execution.
    ///
    /// # Warnings
    ///
    /// Warnings are ignored on stable/beta
    pub fn emit(self) {
        check_correctness();
        crate::imp::emit_diagnostic(self);
    }
}

/// **NOT PUBLIC API! NOTHING TO SEE HERE!!!**
#[doc(hidden)]
impl Diagnostic {
    pub fn span_suggestion(self, span: Span, suggestion: &str, msg: String) -> Self {
        match suggestion {
            "help" | "hint" => self.span_help(span, msg),
            _ => self.span_note(span, msg),
        }
    }

    pub fn suggestion(self, suggestion: &str, msg: String) -> Self {
        match suggestion {
            "help" | "hint" => self.help(msg),
            _ => self.note(msg),
        }
    }
}

impl ToTokens for Diagnostic {
    fn to_tokens(&self, ts: &mut TokenStream) {
        use std::borrow::Cow;

        fn ensure_lf(buf: &mut String, s: &str) {
            if s.ends_with('\n') {
                buf.push_str(s);
            } else {
                buf.push_str(s);
                buf.push('\n');
            }
        }

        fn diag_to_tokens(
            span_range: SpanRange,
            level: &Level,
            msg: &str,
            suggestions: &[(SuggestionKind, String, Option<SpanRange>)],
        ) -> TokenStream {
            if *level == Level::Warning {
                return TokenStream::new();
            }

            let message = if suggestions.is_empty() {
                Cow::Borrowed(msg)
            } else {
                let mut message = String::new();
                ensure_lf(&mut message, msg);
                message.push('\n');

                for (kind, note, _span) in suggestions {
                    message.push_str("  = ");
                    message.push_str(kind.name());
                    message.push_str(": ");
                    ensure_lf(&mut message, note);
                }
                message.push('\n');

                Cow::Owned(message)
            };

            let mut msg = proc_macro2::Literal::string(&message);
            msg.set_span(span_range.last);
            let group = quote_spanned!(span_range.last=> { #msg } );
            quote_spanned!(span_range.first=> compile_error!#group)
        }

        ts.extend(diag_to_tokens(
            self.span_range,
            &self.level,
            &self.msg,
            &self.suggestions,
        ));
        ts.extend(
            self.children
                .iter()
                .map(|(span_range, msg)| diag_to_tokens(*span_range, &Level::Error, msg, &[])),
        );
    }
}

#[derive(Debug)]
pub(crate) enum SuggestionKind {
    Help,
    Note,
}

impl SuggestionKind {
    fn name(&self) -> &'static str {
        match self {
            SuggestionKind::Note => "note",
            SuggestionKind::Help => "help",
        }
    }
}

#[cfg(feature = "syn-error")]
impl From<syn::Error> for Diagnostic {
    fn from(err: syn::Error) -> Self {
        use proc_macro2::{Delimiter, TokenTree};

        fn gut_error(ts: &mut impl Iterator<Item = TokenTree>) -> Option<(SpanRange, String)> {
            let start_span = ts.next()?.span();
            ts.next().expect(":1");
            ts.next().expect("core");
            ts.next().expect(":2");
            ts.next().expect(":3");
            ts.next().expect("compile_error");
            ts.next().expect("!");

            let lit = match ts.next().unwrap() {
                TokenTree::Group(group) => {
                    // Currently `syn` builds `compile_error!` invocations
                    // exclusively in `ident{"..."}` (braced) form which is not
                    // followed by `;` (semicolon).
                    //
                    // But if it changes to `ident("...");` (parenthesized)
                    // or `ident["..."];` (bracketed) form,
                    // we will need to skip the `;` as well.
                    // Highly unlikely, but better safe than sorry.

                    if group.delimiter() == Delimiter::Parenthesis
                        || group.delimiter() == Delimiter::Bracket
                    {
                        ts.next().unwrap(); // ;
                    }

                    match group.stream().into_iter().next().unwrap() {
                        TokenTree::Literal(lit) => lit,
                        _ => unreachable!(""),
                    }
                }
                _ => unreachable!(""),
            };

            let last = lit.span();
            let mut msg = lit.to_string();

            // "abc" => abc
            msg.pop();
            msg.remove(0);

            Some((
                SpanRange {
                    first: start_span,
                    last,
                },
                msg,
            ))
        }

        let mut ts = err.to_compile_error().into_iter();

        let (span_range, msg) = gut_error(&mut ts).unwrap();
        let mut res = Diagnostic::spanned_range(span_range, Level::Error, msg);

        while let Some((span_range, msg)) = gut_error(&mut ts) {
            res = res.span_range_error(span_range, msg);
        }

        res
    }
}
