use darling::ast::NestedMeta;
use std::collections::BTreeSet;
use syn::punctuated::Punctuated;

use pavex_bp_schema::MethodGuard;

#[derive(Debug, Clone, PartialEq, Eq)]
/// The methods supported by a certain route.
pub enum MethodArgument {
    /// One method. E.g. `method = "POST"`.
    Single(String),
    /// Multiple methods. E.g. `method = ["POST", "GET"]`.
    Multiple(Vec<String>),
}

impl From<MethodArgument> for MethodGuard {
    fn from(arg: MethodArgument) -> Self {
        match arg {
            MethodArgument::Single(m) => MethodGuard::Some(BTreeSet::from_iter(std::iter::once(m))),
            MethodArgument::Multiple(vec) => MethodGuard::Some(BTreeSet::from_iter(vec)),
        }
    }
}

impl darling::FromMeta for MethodArgument {
    fn from_nested_meta(meta: &NestedMeta) -> Result<Self, darling::Error> {
        match meta {
            // method = "POST"
            NestedMeta::Lit(syn::Lit::Str(s)) => Ok(MethodArgument::Single(s.value())),

            // method = ["POST", "GET"]
            NestedMeta::Meta(syn::Meta::List(list)) => {
                // Parse the inner list as a punctuated list of string literals
                let items: Punctuated<syn::Lit, syn::token::Comma> =
                    list.parse_args_with(Punctuated::parse_terminated)?;

                let mut methods = Vec::new();
                for lit in items {
                    if let syn::Lit::Str(s) = lit {
                        methods.push(s.value());
                    } else {
                        return Err(
                            darling::Error::custom("Expected a string literal").with_span(&lit)
                        );
                    }
                }
                Ok(MethodArgument::Multiple(methods))
            }

            other => Err(darling::Error::custom("Invalid argument for `method`").with_span(other)),
        }
    }

    fn from_string(value: &str) -> darling::Result<Self> {
        Ok(MethodArgument::Single(value.into()))
    }

    fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
        match *expr {
            syn::Expr::Lit(ref lit) => Self::from_value(&lit.lit),
            syn::Expr::Group(ref group) => {
                // syn may generate this invisible group delimiter when the input to the darling
                // proc macro (specifically, the attributes) are generated by a
                // macro_rules! (e.g. propagating a macro_rules!'s expr)
                // Since we want to basically ignore these invisible group delimiters,
                // we just propagate the call to the inner expression.
                Self::from_expr(&group.expr)
            }
            syn::Expr::Array(ref array) => {
                let mut methods = Vec::new();
                for elem in &array.elems {
                    if let syn::Expr::Lit(syn::ExprLit {
                        lit: syn::Lit::Str(s),
                        ..
                    }) = elem
                    {
                        methods.push(s.value());
                    } else {
                        return Err(
                            darling::Error::custom("Expected a string literal").with_span(elem)
                        );
                    }
                }
                Ok(MethodArgument::Multiple(methods))
            }
            _ => Err(darling::Error::unexpected_expr_type(expr)),
        }
        .map_err(|e| e.with_span(expr))
    }
}

impl quote::ToTokens for MethodArgument {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let new_tokens = match self {
            MethodArgument::Single(method) => {
                quote::quote! { #method }
            }
            MethodArgument::Multiple(methods) => {
                quote::quote! { [ #( #methods ),* ] }
            }
        };
        tokens.extend(new_tokens);
    }
}
