#![allow(clippy::option_if_let_else)]
//! Helper crate that generates customizable Rust `Structs` describing strings.

use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{
    DataEnum, DeriveInput, Error, Fields, FieldsNamed, Ident, Lit, parse_macro_input,
    spanned::Spanned,
};

#[allow(clippy::missing_panics_doc)]
#[proc_macro_derive(Describe, attributes(describe))]
pub fn describe(input: TokenStream) -> TokenStream {
    let DeriveInput {
        ident,
        data,
        generics,
        ..
    } = parse_macro_input!(input);

    if !generics.params.is_empty() {
        return Error::new(
            generics.params.span(),
            "Generics not supported in the current version",
        )
        .to_compile_error()
        .into();
    }
    let description = match data {
        syn::Data::Struct(s) => Ok(match s.fields {
            syn::Fields::Named(FieldsNamed { named, .. }) => {
                let field_descriptions = named.iter().filter_map(|f| {
                    let attrs = parse_field_attrs(&f.attrs);
                    if attrs.skip {
                        return None;
                    }

                    let field_name = f.ident.as_ref().unwrap();
                    let field_name_str = attrs.rename.unwrap_or_else(|| field_name.to_string());

                    let type_description = if let Some(with) = attrs.with {
                        quote! { #with.to_string() }
                    } else {
                        let field_type = &f.ty;
                        quote! { <#field_type>::describe() }
                    };

                    Some(quote! {
                        format!("{}: {}", #field_name_str,  #type_description)
                    })
                });
                quote! {
                    format!("{} {{ {} }}",
                        stringify!(#ident),
                        vec![#(#field_descriptions),*].join(", ")
                    )
                }
            }
            syn::Fields::Unnamed(fields) => {
                let field_descriptions = fields.unnamed.iter().filter_map(|f| {
                    let attrs = parse_field_attrs(&f.attrs);
                    if attrs.skip {
                        return None;
                    }
                    let type_description = if let Some(with) = attrs.with {
                        quote! { #with.to_string() }
                    } else {
                        let field_type = &f.ty;
                        quote! { <#field_type>::describe() }
                    };

                    Some(quote! {
                        #type_description
                    })
                });
                quote! {
                    format!("{}({})",
                        stringify!(#ident),
                        vec![#(#field_descriptions),*].join(", ")
                    )
                }
            }
            syn::Fields::Unit => quote! {
                stringify!(#ident).to_string()
            },
        }),
        syn::Data::Enum(DataEnum { variants, .. }) => Ok(enum_variants(&ident, &variants)),
        syn::Data::Union(_) => Err(Error::new(
            ident.span(),
            "The syn::Data::Union variant is not supported",
        )),
    };

    let description = match description {
        Ok(ok) => ok,
        Err(err) => return err.to_compile_error().into(),
    };

    let expanded = quote! {
        impl Describer for #ident {
            fn describe() -> String {
                #description
            }
        }
    };

    expanded.into()
}

fn enum_variants(
    name: &Ident,
    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
) -> proc_macro2::TokenStream {
    let variant_descriptions = variants.iter().map(|v| {
                let variant_name = &v.ident;
                let variant_name_str = variant_name.to_string();
                match &v.fields {
                    Fields::Named(fields) => {
                        let field_descriptions = fields.named.iter().filter_map(|f| {
                            let attrs = parse_field_attrs(&f.attrs);
                            if attrs.skip {
                                return None;
                            }

                            let field_name = f.ident.as_ref().unwrap();
                            let field_name_str = attrs.rename.unwrap_or_else(|| field_name.to_string());
                            let type_description = if let Some(with) = attrs.with {
                                quote! { #with.to_string() }
                            } else {
                                let field_type = &f.ty;
                                quote! { <#field_type>::describe() }
                            };
                            Some(quote! {
                                format!("{}: {}", #field_name_str, #type_description)
                            })
                        });
                        quote! {
                            format!("{} {{ {} }}", #variant_name_str, vec![#(#field_descriptions),*].join(", "))
                        }
                    }
                    Fields::Unnamed(fields) => {
                        let field_descriptions = fields.unnamed.iter().filter_map(|f| {
                            let attrs = parse_field_attrs(&f.attrs);

                            if attrs.skip {
                                return None;
                            }
                            let type_description = if let Some(with) = attrs.with {
                                quote! { #with.to_string() }
                            } else {
                                let field_type = &f.ty;
                                quote! { <#field_type>::describe() }
                            };
                            Some(quote! {
                                #type_description
                            })
                        });
                        quote! {
                            format!("{}({})", #variant_name_str, vec![#(#field_descriptions),*].join(", "))
                        }
                    }
                    Fields::Unit => {
                        quote! {
                            #variant_name_str.to_string()
                        }
                    }
                }
            });
    quote! {
        format!("{} #{{ {} }}",
            stringify!(#name),
            vec![#(#variant_descriptions),*].join(" | ")
        )
    }
}

struct FieldAttrs {
    skip: bool,
    rename: Option<String>,
    with: Option<String>,
}

fn parse_field_attrs(attrs: &[syn::Attribute]) -> FieldAttrs {
    let mut field_attrs = FieldAttrs {
        skip: false,
        rename: None,
        with: None,
    };

    for attr in attrs {
        if !attr.path().is_ident("describe") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("skip") {
                field_attrs.skip = true;
                Ok(())
            } else if meta.path.is_ident("rename") {
                let value = meta.value()?;
                let s: Lit = value.parse()?;
                if let Lit::Str(lit_str) = s {
                    field_attrs.rename = Some(lit_str.value());
                }
                Ok(())
            } else if meta.path.is_ident("with") {
                let value = meta.value()?;
                let s: Lit = value.parse()?;
                if let Lit::Str(lit_str) = s {
                    field_attrs.with = Some(lit_str.value());
                }
                Ok(())
            } else {
                Err(meta.error("unsupported describe attribute"))
            }
        });
    }

    field_attrs
}
