use proc_macro2::Span;
use syn::{Fields, FieldsNamed, FieldsUnnamed};

use super::*;

#[derive(Clone, Debug, Default)]
struct VariantAttrs
{
    defaulted_variant: bool,
}

impl ParseAttrs<VariantAttrs> for &syn::Variant
{
    fn parse_attrs_into(self, attrs: &mut VariantAttrs) -> syn::Result<()>
    {
        self.attrs.parse_attrs_into(attrs)
    }
}
impl ParseAttrs<VariantAttrs> for &syn::Attribute
{
    fn parse_attrs_into(self, attrs: &mut VariantAttrs) -> syn::Result<()>
    {
        if self.path().is_ident(stringify!(defaulted)) {
            if let Meta::Path(_) = self.meta {
                // plain #[defaulted]
                attrs.defaulted_variant = true;
                return Ok(());
            }
            let meta = self.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
            meta.parse_attrs_into(attrs)?;
        }
        Ok(())
    }
}
impl ParseAttrs<VariantAttrs> for Meta
{
    fn parse_attrs_into(self, attrs: &mut VariantAttrs) -> syn::Result<()>
    {
        match &self {
            Self::Path(path) if path.is_ident("default") => {
                attrs.defaulted_variant = true;
                Ok(())
            }
            _ => Err(syn::Error::new_spanned(self, "Invalid to_derive specifier")),
        }
    }
}

/// Generate inline `defaulted` checks for an enum.
pub fn generate(data: syn::DataEnum) -> TokenStream2
{
    generate_inner(data).unwrap_or_else(syn::Error::into_compile_error)
}

fn generate_inner(data: syn::DataEnum) -> syn::Result<TokenStream2>
{
    for variant in data.variants {
        let variant_attrs: VariantAttrs = (&variant).parse_attrs()?;
        if !variant_attrs.defaulted_variant {
            continue;
        }
        let variant_ident = &variant.ident;
        let span = variant.span();
        return match variant.fields {
            Fields::Unit => Ok(quote_spanned! {span=>
                matches!(self, Self::#variant_ident)
            }),
            Fields::Named(FieldsNamed { named, .. }) => {
                generate_variant_with_fields(variant_ident, span, named)
            }
            Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
                generate_variant_with_fields(variant_ident, span, unnamed)
            }
        };
    }
    Err(syn::Error::new(
        Span::call_site(),
        "Must specify default variant",
    ))
}

fn generate_variant_with_fields<P: Default>(
    variant_ident: &syn::Ident,
    span: Span,
    mut fields: Punctuated<syn::Field, P>,
) -> syn::Result<TokenStream2>
{
    let is_named = fields.first().is_some_and(|f| f.ident.is_some());
    assign_synthetic_idents(&mut fields);
    let field_names: Vec<_> = fields.iter().filter_map(|f| f.ident.clone()).collect();
    let impl_block = generate_field_checks(fields)?;

    Ok(if is_named {
        quote_spanned! {span=>
            if let Self::#variant_ident { #(#field_names,)* } = self {
                #impl_block
            } else {
                false
            }
        }
    } else {
        quote_spanned! {span=>
            if let Self::#variant_ident(#(#field_names,)*) = self {
                #impl_block
            } else {
                false
            }
        }
    })
}

/// Generate inline defaulted checks for destructured enum variant fields.
fn generate_field_checks(fields: impl IntoIterator<Item = syn::Field>)
-> syn::Result<TokenStream2>
{
    use field_parser::FieldChecker;

    let mut tokens = TokenStream2::new();
    for field in fields {
        let attrs: field_parser::FieldAttrs = (&field).parse_attrs()?;
        let field_name = &field.ident;

        let conj = (!tokens.is_empty())
            .then_some(quote! {&&})
            .unwrap_or_default();

        tokens.extend(match attrs.checker {
            FieldChecker::Ignore => continue,
            FieldChecker::Delegate => quote_spanned! {field.span()=>
                #conj ::defaulted::Defaulted::is_defaulted(#field_name)
            },
            FieldChecker::Func(func) => quote_spanned! {field.span()=>
                #conj #func(#field_name)
            },
            FieldChecker::Closure(clos) => quote_spanned! {field.span()=>
                #conj {
                    #[allow(clippy::redundant_closure_call)]
                    let __r = (#clos)(#field_name);
                    __r
                }
            },
            FieldChecker::EqualsValue(ref val) => match val {
                syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Bool(b),
                    ..
                }) => match b.value() {
                    true => quote_spanned! {field.span()=> #conj *field_name },
                    false => quote_spanned! {field.span()=> #conj !*field_name },
                },
                _ => quote_spanned! {field.span()=>
                    #conj *#field_name == #val
                },
            },
            FieldChecker::EqualsRef(ref val) => quote_spanned! {field.span()=>
                #conj #field_name == #val
            },
        });
    }
    if tokens.is_empty() {
        tokens = quote! { true };
    }
    Ok(tokens)
}

/// Ensures every field has an ident, generating `_field_0`, `_field_1`, etc.
/// for unnamed (tuple) fields.
fn assign_synthetic_idents<P>(fields: &mut Punctuated<syn::Field, P>)
{
    for (i, field) in fields.iter_mut().enumerate() {
        if field.ident.is_none() {
            field.ident = Some(syn::Ident::new(&format!("field_{i}"), field.span()));
        }
    }
}
