//! Facility for interpreting structured content inside of an `Attribute`.

use crate::ext::IdentExt;
use crate::lit::Lit;
use crate::parse::{Error, ParseStream, Parser, Result};
use crate::path::{Path, PathSegment};
use crate::punctuated::Punctuated;
use proc_macro2::Ident;
use std::fmt::Display;

/// Make a parser that is usable with `parse_macro_input!` in a
/// `#[proc_macro_attribute]` macro.
///
/// *Warning:* When parsing attribute args **other than** the
/// `proc_macro::TokenStream` input of a `proc_macro_attribute`, you do **not**
/// need this function. In several cases your callers will get worse error
/// messages if you use this function, because the surrounding delimiter's span
/// is concealed from attribute macros by rustc. Use
/// [`Attribute::parse_nested_meta`] instead.
///
/// [`Attribute::parse_nested_meta`]: crate::Attribute::parse_nested_meta
///
/// # Example
///
/// This example implements an attribute macro whose invocations look like this:
///
/// ```
/// # const IGNORE: &str = stringify! {
/// #[tea(kind = "EarlGrey", hot)]
/// struct Picard {...}
/// # };
/// ```
///
/// The "parameters" supported by the attribute are:
///
/// - `kind = "..."`
/// - `hot`
/// - `with(sugar, milk, ...)`, a comma-separated list of ingredients
///
/// ```
/// # extern crate proc_macro;
/// #
/// use proc_macro::TokenStream;
/// use syn::{parse_macro_input, LitStr, Path};
///
/// # const IGNORE: &str = stringify! {
/// #[proc_macro_attribute]
/// # };
/// pub fn tea(args: TokenStream, input: TokenStream) -> TokenStream {
///     let mut kind: Option<LitStr> = None;
///     let mut hot: bool = false;
///     let mut with: Vec<Path> = Vec::new();
///     let tea_parser = syn::meta::parser(|meta| {
///         if meta.path.is_ident("kind") {
///             kind = Some(meta.value()?.parse()?);
///             Ok(())
///         } else if meta.path.is_ident("hot") {
///             hot = true;
///             Ok(())
///         } else if meta.path.is_ident("with") {
///             meta.parse_nested_meta(|meta| {
///                 with.push(meta.path);
///                 Ok(())
///             })
///         } else {
///             Err(meta.error("unsupported tea property"))
///         }
///     });
///
///     parse_macro_input!(args with tea_parser);
///     eprintln!("kind={kind:?} hot={hot} with={with:?}");
///
///     /* ... */
/// #   TokenStream::new()
/// }
/// ```
///
/// The `syn::meta` library will take care of dealing with the commas including
/// trailing commas, and producing sensible error messages on unexpected input.
///
/// ```console
/// error: expected `,`
///  --> src/main.rs:3:37
///   |
/// 3 | #[tea(kind = "EarlGrey", with(sugar = "lol", milk))]
///   |                                     ^
/// ```
///
/// # Example
///
/// Same as above but we factor out most of the logic into a separate function.
///
/// ```
/// # extern crate proc_macro;
/// #
/// use proc_macro::TokenStream;
/// use syn::meta::ParseNestedMeta;
/// use syn::parse::{Parser, Result};
/// use syn::{parse_macro_input, LitStr, Path};
///
/// # const IGNORE: &str = stringify! {
/// #[proc_macro_attribute]
/// # };
/// pub fn tea(args: TokenStream, input: TokenStream) -> TokenStream {
///     let mut attrs = TeaAttributes::default();
///     let tea_parser = syn::meta::parser(|meta| attrs.parse(meta));
///     parse_macro_input!(args with tea_parser);
///
///     /* ... */
/// #   TokenStream::new()
/// }
///
/// #[derive(Default)]
/// struct TeaAttributes {
///     kind: Option<LitStr>,
///     hot: bool,
///     with: Vec<Path>,
/// }
///
/// impl TeaAttributes {
///     fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
///         if meta.path.is_ident("kind") {
///             self.kind = Some(meta.value()?.parse()?);
///             Ok(())
///         } else /* just like in last example */
/// #           { unimplemented!() }
///
///     }
/// }
/// ```
pub fn parser(logic: impl FnMut(ParseNestedMeta) -> Result<()>) -> impl Parser<Output = ()> {
    |input: ParseStream| parse_nested_meta(input, logic)
}

/// Context for parsing a single property in the conventional syntax for
/// structured attributes.
///
/// # Examples
///
/// Refer to usage examples on the following two entry-points:
///
/// - [`Attribute::parse_nested_meta`] if you have an entire `Attribute` to
///   parse. Always use this if possible. Generally this is able to produce
///   better error messages because `Attribute` holds span information for all
///   of the delimiters therein.
///
/// - [`syn::meta::parser`] if you are implementing a `proc_macro_attribute`
///   macro and parsing the arguments to the attribute macro, i.e. the ones
///   written in the same attribute that dispatched the macro invocation. Rustc
///   does not pass span information for the surrounding delimiters into the
///   attribute macro invocation in this situation, so error messages might be
///   less precise.
///
/// [`Attribute::parse_nested_meta`]: crate::Attribute::parse_nested_meta
/// [`syn::meta::parser`]: crate::meta::parser
#[non_exhaustive]
pub struct ParseNestedMeta<'a> {
    pub path: Path,
    pub input: ParseStream<'a>,
}

impl<'a> ParseNestedMeta<'a> {
    /// Used when parsing `key = "value"` syntax.
    ///
    /// All it does is advance `meta.input` past the `=` sign in the input. You
    /// could accomplish the same effect by writing
    /// `meta.parse::<Token![=]>()?`, so at most it is a minor convenience to
    /// use `meta.value()?`.
    ///
    /// # Example
    ///
    /// ```
    /// use syn::{parse_quote, Attribute, LitStr};
    ///
    /// let attr: Attribute = parse_quote! {
    ///     #[tea(kind = "EarlGrey")]
    /// };
    ///                                          // conceptually:
    /// if attr.path().is_ident("tea") {         // this parses the `tea`
    ///     attr.parse_nested_meta(|meta| {      // this parses the `(`
    ///         if meta.path.is_ident("kind") {  // this parses the `kind`
    ///             let value = meta.value()?;   // this parses the `=`
    ///             let s: LitStr = value.parse()?;  // this parses `"EarlGrey"`
    ///             if s.value() == "EarlGrey" {
    ///                 // ...
    ///             }
    ///             Ok(())
    ///         } else {
    ///             Err(meta.error("unsupported attribute"))
    ///         }
    ///     })?;
    /// }
    /// # anyhow::Ok(())
    /// ```
    pub fn value(&self) -> Result<ParseStream<'a>> {
        self.input.parse::<Token![=]>()?;
        Ok(self.input)
    }

    /// Used when parsing `list(...)` syntax **if** the content inside the
    /// nested parentheses is also expected to conform to Rust's structured
    /// attribute convention.
    ///
    /// # Example
    ///
    /// ```
    /// use syn::{parse_quote, Attribute};
    ///
    /// let attr: Attribute = parse_quote! {
    ///     #[tea(with(sugar, milk))]
    /// };
    ///
    /// if attr.path().is_ident("tea") {
    ///     attr.parse_nested_meta(|meta| {
    ///         if meta.path.is_ident("with") {
    ///             meta.parse_nested_meta(|meta| {  // <---
    ///                 if meta.path.is_ident("sugar") {
    ///                     // Here we can go even deeper if needed.
    ///                     Ok(())
    ///                 } else if meta.path.is_ident("milk") {
    ///                     Ok(())
    ///                 } else {
    ///                     Err(meta.error("unsupported ingredient"))
    ///                 }
    ///             })
    ///         } else {
    ///             Err(meta.error("unsupported tea property"))
    ///         }
    ///     })?;
    /// }
    /// # anyhow::Ok(())
    /// ```
    ///
    /// # Counterexample
    ///
    /// If you don't need `parse_nested_meta`'s help in parsing the content
    /// written within the nested parentheses, keep in mind that you can always
    /// just parse it yourself from the exposed ParseStream. Rust syntax permits
    /// arbitrary tokens within those parentheses so for the crazier stuff,
    /// `parse_nested_meta` is not what you want.
    ///
    /// ```
    /// use syn::{parenthesized, parse_quote, Attribute, LitInt};
    ///
    /// let attr: Attribute = parse_quote! {
    ///     #[repr(align(32))]
    /// };
    ///
    /// let mut align: Option<LitInt> = None;
    /// if attr.path().is_ident("repr") {
    ///     attr.parse_nested_meta(|meta| {
    ///         if meta.path.is_ident("align") {
    ///             let content;
    ///             parenthesized!(content in meta.input);
    ///             align = Some(content.parse()?);
    ///             Ok(())
    ///         } else {
    ///             Err(meta.error("unsupported repr"))
    ///         }
    ///     })?;
    /// }
    /// # anyhow::Ok(())
    /// ```
    pub fn parse_nested_meta(
        &self,
        logic: impl FnMut(ParseNestedMeta) -> Result<()>,
    ) -> Result<()> {
        let content;
        parenthesized!(content in self.input);
        parse_nested_meta(&content, logic)
    }

    /// Report that the attribute's content did not conform to expectations.
    ///
    /// The span of the resulting error will cover `meta.path` *and* everything
    /// that has been parsed so far since it.
    ///
    /// There are 2 ways you might call this. First, if `meta.path` is not
    /// something you recognize:
    ///
    /// ```
    /// # use syn::Attribute;
    /// #
    /// # fn example(attr: &Attribute) -> syn::Result<()> {
    /// attr.parse_nested_meta(|meta| {
    ///     if meta.path.is_ident("kind") {
    ///         // ...
    ///         Ok(())
    ///     } else {
    ///         Err(meta.error("unsupported tea property"))
    ///     }
    /// })?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// In this case, it behaves exactly like
    /// `syn::Error::new_spanned(&meta.path, "message...")`.
    ///
    /// ```console
    /// error: unsupported tea property
    ///  --> src/main.rs:3:26
    ///   |
    /// 3 | #[tea(kind = "EarlGrey", wat = "foo")]
    ///   |                          ^^^
    /// ```
    ///
    /// More usefully, the second place is if you've already parsed a value but
    /// have decided not to accept the value:
    ///
    /// ```
    /// # use syn::Attribute;
    /// #
    /// # fn example(attr: &Attribute) -> syn::Result<()> {
    /// use syn::Expr;
    ///
    /// attr.parse_nested_meta(|meta| {
    ///     if meta.path.is_ident("kind") {
    ///         let expr: Expr = meta.value()?.parse()?;
    ///         match expr {
    ///             Expr::Lit(expr) => /* ... */
    /// #               unimplemented!(),
    ///             Expr::Path(expr) => /* ... */
    /// #               unimplemented!(),
    ///             Expr::Macro(expr) => /* ... */
    /// #               unimplemented!(),
    ///             _ => Err(meta.error("tea kind must be a string literal, path, or macro")),
    ///         }
    ///     } else /* as above */
    /// #       { unimplemented!() }
    ///
    /// })?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// ```console
    /// error: tea kind must be a string literal, path, or macro
    ///  --> src/main.rs:3:7
    ///   |
    /// 3 | #[tea(kind = async { replicator.await })]
    ///   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    /// ```
    ///
    /// Often you may want to use `syn::Error::new_spanned` even in this
    /// situation. In the above code, that would be:
    ///
    /// ```
    /// # use syn::{Error, Expr};
    /// #
    /// # fn example(expr: Expr) -> syn::Result<()> {
    ///     match expr {
    ///         Expr::Lit(expr) => /* ... */
    /// #           unimplemented!(),
    ///         Expr::Path(expr) => /* ... */
    /// #           unimplemented!(),
    ///         Expr::Macro(expr) => /* ... */
    /// #           unimplemented!(),
    ///         _ => Err(Error::new_spanned(expr, "unsupported expression type for `kind`")),
    ///     }
    /// # }
    /// ```
    ///
    /// ```console
    /// error: unsupported expression type for `kind`
    ///  --> src/main.rs:3:14
    ///   |
    /// 3 | #[tea(kind = async { replicator.await })]
    ///   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^
    /// ```
    pub fn error(&self, msg: impl Display) -> Error {
        let start_span = self.path.segments[0].ident.span();
        let end_span = self.input.cursor().prev_span();
        crate::error::new2(start_span, end_span, msg)
    }
}

pub(crate) fn parse_nested_meta(
    input: ParseStream,
    mut logic: impl FnMut(ParseNestedMeta) -> Result<()>,
) -> Result<()> {
    loop {
        let path = input.call(parse_meta_path)?;
        logic(ParseNestedMeta { path, input })?;
        if input.is_empty() {
            return Ok(());
        }
        input.parse::<Token![,]>()?;
        if input.is_empty() {
            return Ok(());
        }
    }
}

// Like Path::parse_mod_style, but accepts keywords in the path.
fn parse_meta_path(input: ParseStream) -> Result<Path> {
    Ok(Path {
        leading_colon: input.parse()?,
        segments: {
            let mut segments = Punctuated::new();
            if input.peek(Ident::peek_any) {
                let ident = Ident::parse_any(input)?;
                segments.push_value(PathSegment::from(ident));
            } else if input.is_empty() {
                return Err(input.error("expected nested attribute"));
            } else if input.peek(Lit) {
                return Err(input.error("unexpected literal in nested attribute, expected ident"));
            } else {
                return Err(input.error("unexpected token in nested attribute, expected ident"));
            }
            while input.peek(Token![::]) {
                let punct = input.parse()?;
                segments.push_punct(punct);
                let ident = Ident::parse_any(input)?;
                segments.push_value(PathSegment::from(ident));
            }
            segments
        },
    })
}
