use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{LitInt, LitStr};

enum Error {
    InputNotEnum,
    Syn(syn::Error),
}

struct Display {
    ident: syn::Ident,
    variants: Vec<DisplayVariant>,
}

struct DisplayVariant {
    ident: syn::Ident,
    display_value: TokenStream,
}

impl Display {
    fn new(input: syn::DeriveInput) -> Result<Self, Error> {
        let ident = input.ident;

        let variant_vec = match input.data {
            syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
            _ => return Err(Error::InputNotEnum),
        };

        let mut variants = Vec::new();
        for variant in variant_vec {
            let mut display_value = variant.ident.to_string().to_token_stream();
            for attr in variant.attrs.iter() {
                if !attr.path().is_ident("sea_orm") {
                    continue;
                }
                attr.parse_nested_meta(|meta| {
                    if meta.path.is_ident("string_value") {
                        meta.value()?.parse::<LitStr>()?;
                    } else if meta.path.is_ident("num_value") {
                        meta.value()?.parse::<LitInt>()?;
                    } else if meta.path.is_ident("display_value") {
                        display_value = meta.value()?.parse::<LitStr>()?.to_token_stream();
                    } else {
                        return Err(meta.error(format!(
                            "Unknown attribute parameter found: {:?}",
                            meta.path.get_ident()
                        )));
                    }

                    Ok(())
                })
                .map_err(Error::Syn)?;
            }
            variants.push(DisplayVariant {
                ident: variant.ident,
                display_value,
            });
        }
        Ok(Display { ident, variants })
    }

    fn expand(&self) -> syn::Result<TokenStream> {
        let expanded_impl_active_enum_display = self.impl_active_enum_display();

        Ok(expanded_impl_active_enum_display)
    }

    fn impl_active_enum_display(&self) -> TokenStream {
        let Self { ident, variants } = self;

        let variant_idents: Vec<_> = variants
            .iter()
            .map(|variant| variant.ident.clone())
            .collect();

        let variant_display: Vec<_> = variants
            .iter()
            .map(|variant| variant.display_value.to_owned())
            .collect();

        quote!(
            #[automatically_derived]
            impl std::fmt::Display for #ident {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    write!(f, "{}", match self {
                        #( Self::#variant_idents => #variant_display, )*
                    })
                }
            }
        )
    }
}

pub fn expand_derive_active_enum_display(input: syn::DeriveInput) -> syn::Result<TokenStream> {
    let ident_span = input.ident.span();

    match Display::new(input) {
        Ok(model) => model.expand(),
        Err(Error::InputNotEnum) => Ok(quote_spanned! {
            ident_span => compile_error!("you can only derive EnumDisplay on enums");
        }),
        Err(Error::Syn(e)) => Err(e),
    }
}
