use rustc_ast as ast;
use rustc_ast::token::{self, MetaVarKind};
use rustc_ast::tokenstream::ParserRange;
use rustc_ast::{AttrItemKind, Attribute, attr};
use rustc_errors::codes::*;
use rustc_errors::{Diag, PResult, msg};
use rustc_span::{BytePos, Span};
use thin_vec::ThinVec;
use tracing::debug;

use super::{
    AllowConstBlockItems, AttrWrapper, Capturing, FnParseMode, ForceCollect, Parser, PathStyle,
    Trailing, UsePreAttrPos,
};
use crate::parser::FnContext;
use crate::{errors, exp};

// Public for rustfmt usage
#[derive(Debug)]
pub enum InnerAttrPolicy {
    Permitted,
    Forbidden(Option<InnerAttrForbiddenReason>),
}

#[derive(Clone, Copy, Debug)]
pub enum InnerAttrForbiddenReason {
    InCodeBlock,
    AfterOuterDocComment { prev_doc_comment_span: Span },
    AfterOuterAttribute { prev_outer_attr_sp: Span },
}

enum OuterAttributeType {
    DocComment,
    DocBlockComment,
    Attribute,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AllowLeadingUnsafe {
    Yes,
    No,
}

impl<'a> Parser<'a> {
    /// Parses attributes that appear before an item.
    pub(super) fn parse_outer_attributes(&mut self) -> PResult<'a, AttrWrapper> {
        let mut outer_attrs = ast::AttrVec::new();
        let mut just_parsed_doc_comment = false;
        let start_pos = self.num_bump_calls;
        loop {
            let attr = if self.check(exp!(Pound)) {
                let prev_outer_attr_sp = outer_attrs.last().map(|attr: &Attribute| attr.span);

                let inner_error_reason = if just_parsed_doc_comment {
                    Some(InnerAttrForbiddenReason::AfterOuterDocComment {
                        prev_doc_comment_span: prev_outer_attr_sp.unwrap(),
                    })
                } else {
                    prev_outer_attr_sp.map(|prev_outer_attr_sp| {
                        InnerAttrForbiddenReason::AfterOuterAttribute { prev_outer_attr_sp }
                    })
                };
                let inner_parse_policy = InnerAttrPolicy::Forbidden(inner_error_reason);
                just_parsed_doc_comment = false;
                Some(self.parse_attribute(inner_parse_policy)?)
            } else if let token::DocComment(comment_kind, attr_style, data) = self.token.kind {
                if attr_style != ast::AttrStyle::Outer {
                    let span = self.token.span;
                    let mut err =
                        self.dcx().struct_span_err(span, msg!("expected outer doc comment"));
                    err.code(E0753);
                    if let Some(replacement_span) = self.annotate_following_item_if_applicable(
                        &mut err,
                        span,
                        match comment_kind {
                            token::CommentKind::Line => OuterAttributeType::DocComment,
                            token::CommentKind::Block => OuterAttributeType::DocBlockComment,
                        },
                        true,
                    ) {
                        err.note(msg!(
                            "inner doc comments like this (starting with `//!` or `/*!`) can only appear before items"
                        ));
                        err.span_suggestion_verbose(
                            replacement_span,
                            msg!("you might have meant to write a regular comment"),
                            "",
                            rustc_errors::Applicability::MachineApplicable,
                        );
                    }
                    err.emit();
                }
                self.bump();
                just_parsed_doc_comment = true;
                // Always make an outer attribute - this allows us to recover from a misplaced
                // inner attribute.
                Some(attr::mk_doc_comment(
                    &self.psess.attr_id_generator,
                    comment_kind,
                    ast::AttrStyle::Outer,
                    data,
                    self.prev_token.span,
                ))
            } else {
                None
            };

            if let Some(attr) = attr {
                if attr.style == ast::AttrStyle::Outer {
                    outer_attrs.push(attr);
                }
            } else {
                break;
            }
        }
        Ok(AttrWrapper::new(outer_attrs, start_pos))
    }

    /// Matches `attribute = # ! [ meta_item ]`.
    /// `inner_parse_policy` prescribes how to handle inner attributes.
    // Public for rustfmt usage.
    pub fn parse_attribute(
        &mut self,
        inner_parse_policy: InnerAttrPolicy,
    ) -> PResult<'a, ast::Attribute> {
        debug!(
            "parse_attribute: inner_parse_policy={:?} self.token={:?}",
            inner_parse_policy, self.token
        );
        let lo = self.token.span;
        // Attributes can't have attributes of their own [Editor's note: not with that attitude]
        self.collect_tokens_no_attrs(|this| {
            let pound_hi = this.token.span.hi();
            assert!(this.eat(exp!(Pound)), "parse_attribute called in non-attribute position");

            let not_lo = this.token.span.lo();
            let style =
                if this.eat(exp!(Bang)) { ast::AttrStyle::Inner } else { ast::AttrStyle::Outer };

            let mut bracket_res = this.expect(exp!(OpenBracket));
            // If `#!` is not followed by `[`
            if let Err(err) = &mut bracket_res
                && style == ast::AttrStyle::Inner
                && pound_hi == not_lo
            {
                err.note(
                    "the token sequence `#!` here looks like the start of \
                    a shebang interpreter directive but it is not",
                );
                err.help(
                    "if you meant this to be a shebang interpreter directive, \
                    move it to the very start of the file",
                );
            }
            bracket_res?;
            let item = this.parse_attr_item(ForceCollect::No)?;
            this.expect(exp!(CloseBracket))?;
            let attr_sp = lo.to(this.prev_token.span);

            // Emit error if inner attribute is encountered and forbidden.
            if style == ast::AttrStyle::Inner {
                this.error_on_forbidden_inner_attr(
                    attr_sp,
                    inner_parse_policy,
                    item.is_valid_for_outer_style(),
                );
            }

            Ok(attr::mk_attr_from_item(&self.psess.attr_id_generator, item, None, style, attr_sp))
        })
    }

    fn annotate_following_item_if_applicable(
        &self,
        err: &mut Diag<'_>,
        span: Span,
        attr_type: OuterAttributeType,
        suggest_to_outer: bool,
    ) -> Option<Span> {
        let mut snapshot = self.create_snapshot_for_diagnostic();
        let lo = span.lo()
            + BytePos(match attr_type {
                OuterAttributeType::Attribute => 1,
                _ => 2,
            });
        let hi = lo + BytePos(1);
        let replacement_span = span.with_lo(lo).with_hi(hi);
        if let OuterAttributeType::DocBlockComment | OuterAttributeType::DocComment = attr_type {
            snapshot.bump();
        }
        loop {
            // skip any other attributes, we want the item
            if snapshot.token == token::Pound {
                if let Err(err) = snapshot.parse_attribute(InnerAttrPolicy::Permitted) {
                    err.cancel();
                    return Some(replacement_span);
                }
            } else {
                break;
            }
        }
        match snapshot.parse_item_common(
            AttrWrapper::empty(),
            true,
            false,
            FnParseMode { req_name: |_, _| true, context: FnContext::Free, req_body: true },
            ForceCollect::No,
            AllowConstBlockItems::Yes,
        ) {
            Ok(Some(item)) => {
                err.arg("item", item.kind.descr());
                err.span_label(
                    item.span,
                    match attr_type {
                        OuterAttributeType::Attribute => {
                            msg!("the inner attribute doesn't annotate this {$item}")
                        }
                        OuterAttributeType::DocComment | OuterAttributeType::DocBlockComment => {
                            msg!("the inner doc comment doesn't annotate this {$item}")
                        }
                    },
                );
                if suggest_to_outer {
                    err.span_suggestion_verbose(
                        replacement_span,
                        match attr_type {
                            OuterAttributeType::Attribute =>  msg!("to annotate the {$item}, change the attribute from inner to outer style"),
                            OuterAttributeType::DocComment | OuterAttributeType::DocBlockComment =>  msg!("to annotate the {$item}, change the doc comment from inner to outer style"),
                        },
                        match attr_type {
                            OuterAttributeType::Attribute => "",
                            OuterAttributeType::DocBlockComment => "*",
                            OuterAttributeType::DocComment => "/",
                        },
                        rustc_errors::Applicability::MachineApplicable,
                    );
                }
                return None;
            }
            Err(item_err) => {
                item_err.cancel();
            }
            Ok(None) => {}
        }
        Some(replacement_span)
    }

    pub(super) fn error_on_forbidden_inner_attr(
        &self,
        attr_sp: Span,
        policy: InnerAttrPolicy,
        suggest_to_outer: bool,
    ) {
        if let InnerAttrPolicy::Forbidden(reason) = policy {
            let mut diag = match reason.as_ref().copied() {
                Some(InnerAttrForbiddenReason::AfterOuterDocComment { prev_doc_comment_span }) => {
                    self.dcx()
                        .struct_span_err(
                            attr_sp,
                            msg!(
                                "an inner attribute is not permitted following an outer doc comment"
                            ),
                        )
                        .with_span_label(
                            attr_sp,
                            msg!("not permitted following an outer doc comment"),
                        )
                        .with_span_label(prev_doc_comment_span, msg!("previous doc comment"))
                }
                Some(InnerAttrForbiddenReason::AfterOuterAttribute { prev_outer_attr_sp }) => self
                    .dcx()
                    .struct_span_err(
                        attr_sp,
                        msg!("an inner attribute is not permitted following an outer attribute"),
                    )
                    .with_span_label(attr_sp, msg!("not permitted following an outer attribute"))
                    .with_span_label(prev_outer_attr_sp, msg!("previous outer attribute")),
                Some(InnerAttrForbiddenReason::InCodeBlock) | None => self.dcx().struct_span_err(
                    attr_sp,
                    msg!("an inner attribute is not permitted in this context"),
                ),
            };

            diag.note(msg!("inner attributes, like `#![no_std]`, annotate the item enclosing them, and are usually found at the beginning of source files"));
            if self
                .annotate_following_item_if_applicable(
                    &mut diag,
                    attr_sp,
                    OuterAttributeType::Attribute,
                    suggest_to_outer,
                )
                .is_some()
            {
                diag.note(msg!(
                    "outer attributes, like `#[test]`, annotate the item following them"
                ));
            };
            diag.emit();
        }
    }

    /// Parses an inner part of an attribute (the path and following tokens).
    /// The tokens must be either a delimited token stream, or empty token stream,
    /// or the "legacy" key-value form.
    ///     PATH `(` TOKEN_STREAM `)`
    ///     PATH `[` TOKEN_STREAM `]`
    ///     PATH `{` TOKEN_STREAM `}`
    ///     PATH
    ///     PATH `=` UNSUFFIXED_LIT
    /// The delimiters or `=` are still put into the resulting token stream.
    pub fn parse_attr_item(&mut self, force_collect: ForceCollect) -> PResult<'a, ast::AttrItem> {
        if let Some(item) = self.eat_metavar_seq_with_matcher(
            |mv_kind| matches!(mv_kind, MetaVarKind::Meta { .. }),
            |this| this.parse_attr_item(force_collect),
        ) {
            return Ok(item);
        }

        // Attr items don't have attributes.
        self.collect_tokens(None, AttrWrapper::empty(), force_collect, |this, _empty_attrs| {
            let is_unsafe = this.eat_keyword(exp!(Unsafe));
            let unsafety = if is_unsafe {
                let unsafe_span = this.prev_token.span;
                this.expect(exp!(OpenParen))?;
                ast::Safety::Unsafe(unsafe_span)
            } else {
                ast::Safety::Default
            };

            let path = this.parse_path(PathStyle::Mod)?;
            let args = this.parse_attr_args()?;
            if is_unsafe {
                this.expect(exp!(CloseParen))?;
            }
            Ok((
                ast::AttrItem { unsafety, path, args: AttrItemKind::Unparsed(args), tokens: None },
                Trailing::No,
                UsePreAttrPos::No,
            ))
        })
    }

    /// Parses attributes that appear after the opening of an item. These should
    /// be preceded by an exclamation mark, but we accept and warn about one
    /// terminated by a semicolon.
    ///
    /// Matches `inner_attrs*`.
    pub fn parse_inner_attributes(&mut self) -> PResult<'a, ast::AttrVec> {
        let mut attrs = ast::AttrVec::new();
        loop {
            let start_pos = self.num_bump_calls;
            // Only try to parse if it is an inner attribute (has `!`).
            let attr = if self.check(exp!(Pound)) && self.look_ahead(1, |t| t == &token::Bang) {
                Some(self.parse_attribute(InnerAttrPolicy::Permitted)?)
            } else if let token::DocComment(comment_kind, attr_style, data) = self.token.kind {
                if attr_style == ast::AttrStyle::Inner {
                    self.bump();
                    Some(attr::mk_doc_comment(
                        &self.psess.attr_id_generator,
                        comment_kind,
                        attr_style,
                        data,
                        self.prev_token.span,
                    ))
                } else {
                    None
                }
            } else {
                None
            };
            if let Some(attr) = attr {
                // If we are currently capturing tokens (i.e. we are within a call to
                // `Parser::collect_tokens`) record the token positions of this inner attribute,
                // for possible later processing in a `LazyAttrTokenStream`.
                if let Capturing::Yes = self.capture_state.capturing {
                    let end_pos = self.num_bump_calls;
                    let parser_range = ParserRange(start_pos..end_pos);
                    self.capture_state.inner_attr_parser_ranges.insert(attr.id, parser_range);
                }
                attrs.push(attr);
            } else {
                break;
            }
        }
        Ok(attrs)
    }

    // Note: must be unsuffixed.
    pub(crate) fn parse_unsuffixed_meta_item_lit(&mut self) -> PResult<'a, ast::MetaItemLit> {
        let lit = self.parse_meta_item_lit()?;
        debug!("checking if {:?} is unsuffixed", lit);

        if !lit.kind.is_unsuffixed() {
            self.dcx().emit_err(errors::SuffixedLiteralInAttribute { span: lit.span });
        }

        Ok(lit)
    }

    /// Matches `COMMASEP(meta_item_inner)`.
    pub fn parse_meta_seq_top(&mut self) -> PResult<'a, ThinVec<ast::MetaItemInner>> {
        // Presumably, the majority of the time there will only be one attr.
        let mut nmis = ThinVec::with_capacity(1);
        while self.token != token::Eof {
            nmis.push(self.parse_meta_item_inner()?);
            if !self.eat(exp!(Comma)) {
                break;
            }
        }
        Ok(nmis)
    }

    /// Parse a meta item per RFC 1559.
    ///
    /// ```ebnf
    /// MetaItem = SimplePath ( '=' UNSUFFIXED_LIT | '(' MetaSeq? ')' )? ;
    /// MetaSeq = MetaItemInner (',' MetaItemInner)* ','? ;
    /// ```
    pub fn parse_meta_item(
        &mut self,
        unsafe_allowed: AllowLeadingUnsafe,
    ) -> PResult<'a, ast::MetaItem> {
        if let Some(MetaVarKind::Meta { has_meta_form }) = self.token.is_metavar_seq() {
            return if has_meta_form {
                let attr_item = self
                    .eat_metavar_seq(MetaVarKind::Meta { has_meta_form: true }, |this| {
                        this.parse_attr_item(ForceCollect::No)
                    })
                    .unwrap();
                Ok(attr_item.meta(attr_item.path.span).unwrap())
            } else {
                self.unexpected_any()
            };
        }

        let lo = self.token.span;
        let is_unsafe = if unsafe_allowed == AllowLeadingUnsafe::Yes {
            self.eat_keyword(exp!(Unsafe))
        } else {
            false
        };
        let unsafety = if is_unsafe {
            let unsafe_span = self.prev_token.span;
            self.expect(exp!(OpenParen))?;

            ast::Safety::Unsafe(unsafe_span)
        } else {
            ast::Safety::Default
        };

        let path = self.parse_path(PathStyle::Mod)?;
        let kind = self.parse_meta_item_kind()?;
        if is_unsafe {
            self.expect(exp!(CloseParen))?;
        }
        let span = lo.to(self.prev_token.span);

        Ok(ast::MetaItem { unsafety, path, kind, span })
    }

    pub(crate) fn parse_meta_item_kind(&mut self) -> PResult<'a, ast::MetaItemKind> {
        Ok(if self.eat(exp!(Eq)) {
            ast::MetaItemKind::NameValue(self.parse_unsuffixed_meta_item_lit()?)
        } else if self.check(exp!(OpenParen)) {
            let (list, _) = self.parse_paren_comma_seq(|p| p.parse_meta_item_inner())?;
            ast::MetaItemKind::List(list)
        } else {
            ast::MetaItemKind::Word
        })
    }

    /// Parse an inner meta item per RFC 1559.
    ///
    /// ```ebnf
    /// MetaItemInner = UNSUFFIXED_LIT | MetaItem ;
    /// ```
    pub fn parse_meta_item_inner(&mut self) -> PResult<'a, ast::MetaItemInner> {
        match self.parse_unsuffixed_meta_item_lit() {
            Ok(lit) => return Ok(ast::MetaItemInner::Lit(lit)),
            Err(err) => err.cancel(), // we provide a better error below
        }

        match self.parse_meta_item(AllowLeadingUnsafe::No) {
            Ok(mi) => return Ok(ast::MetaItemInner::MetaItem(mi)),
            Err(err) => err.cancel(), // we provide a better error below
        }

        let mut err = errors::InvalidMetaItem {
            span: self.token.span,
            descr: super::token_descr(&self.token),
            quote_ident_sugg: None,
        };

        // Suggest quoting idents, e.g. in `#[cfg(key = value)]`. We don't use `Token::ident` and
        // don't `uninterpolate` the token to avoid suggesting anything butchered or questionable
        // when macro metavariables are involved.
        if self.prev_token == token::Eq
            && let token::Ident(..) = self.token.kind
        {
            let before = self.token.span.shrink_to_lo();
            while let token::Ident(..) = self.token.kind {
                self.bump();
            }
            err.quote_ident_sugg = Some(errors::InvalidMetaItemQuoteIdentSugg {
                before,
                after: self.prev_token.span.shrink_to_hi(),
            });
        }

        Err(self.dcx().create_err(err))
    }
}
