use crate::{Error, Result};
use proc_macro2::{Delimiter, Ident, Span, TokenStream, TokenTree};
use quote::ToTokens;
use std::iter::Peekable;

pub(crate) fn parse_input(
    input: TokenStream,
) -> Result<(Vec<Attribute>, Vec<TokenTree>, TokenTree)> {
    let mut input = input.into_iter().peekable();
    let mut attrs = Vec::new();

    while let Some(attr) = parse_next_attr(&mut input)? {
        attrs.push(attr);
    }

    let sig = parse_signature(&mut input);
    let body = input.next().ok_or_else(|| {
        Error::new(
            Span::call_site(),
            "`#[proc_macro_error]` can be applied only to functions".to_string(),
        )
    })?;

    Ok((attrs, sig, body))
}

fn parse_next_attr(
    input: &mut Peekable<impl Iterator<Item = TokenTree>>,
) -> Result<Option<Attribute>> {
    let shebang = match input.peek() {
        Some(TokenTree::Punct(ref punct)) if punct.as_char() == '#' => input.next().unwrap(),
        _ => return Ok(None),
    };

    let group = match input.peek() {
        Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => {
            let res = group.clone();
            input.next();
            res
        }
        other => {
            let span = other.map_or(Span::call_site(), TokenTree::span);
            return Err(Error::new(span, "expected `[`".to_string()));
        }
    };

    let path = match group.stream().into_iter().next() {
        Some(TokenTree::Ident(ident)) => Some(ident),
        _ => None,
    };

    Ok(Some(Attribute {
        shebang,
        group: TokenTree::Group(group),
        path,
    }))
}

fn parse_signature(input: &mut Peekable<impl Iterator<Item = TokenTree>>) -> Vec<TokenTree> {
    let mut sig = Vec::new();
    loop {
        match input.peek() {
            Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => {
                return sig;
            }
            None => return sig,
            _ => sig.push(input.next().unwrap()),
        }
    }
}

pub(crate) struct Attribute {
    pub(crate) shebang: TokenTree,
    pub(crate) group: TokenTree,
    pub(crate) path: Option<Ident>,
}

impl Attribute {
    pub(crate) fn path_is_ident(&self, ident: &str) -> bool {
        self.path.as_ref().map_or(false, |p| *p == ident)
    }
}

impl ToTokens for Attribute {
    fn to_tokens(&self, ts: &mut TokenStream) {
        self.shebang.to_tokens(ts);
        self.group.to_tokens(ts);
    }
}
