use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, format_ident, quote};
use syn::{Ident, parse_quote};

use crate::ir::{IrEnum, IrEnumVariant};

use super::{
    doc_attrs,
    naming::{CodegenIdent, CodegenTypeName},
};

#[derive(Clone, Copy, Debug)]
pub struct CodegenEnum<'a> {
    name: CodegenTypeName<'a>,
    ty: &'a IrEnum<'a>,
}

impl<'a> CodegenEnum<'a> {
    pub fn new(name: CodegenTypeName<'a>, ty: &'a IrEnum<'a>) -> Self {
        Self { name, ty }
    }
}

impl ToTokens for CodegenEnum<'_> {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let mut variants = Vec::new();
        let mut display_arms = Vec::new();
        let mut from_str_arms = Vec::new();

        for variant in &self.ty.variants {
            match variant {
                IrEnumVariant::String(json_value) => {
                    let variant_name = CodegenIdent::Variant(json_value);
                    variants.push(quote! { #variant_name });
                    display_arms.push(quote! { Self::#variant_name => #json_value });
                    from_str_arms.push(quote! { #json_value => Self::#variant_name });
                }
            }
        }

        // The catch-all `Other` variant comes last.
        let type_name: Ident = {
            let name = self.name;
            parse_quote!(#name)
        };
        let other_name = format_ident!("Other{}", type_name);
        variants.push(quote! {
            #[default]
            #other_name
        });
        display_arms.push(quote! { Self::#other_name => "(other)" });
        from_str_arms.push(quote! { _ => Self::#other_name });

        let other_serialize_error = format!("can't serialize variant `{type_name}::{other_name}`");
        let expecting = format!("a variant of `{type_name}`");

        let doc_attrs = self.ty.description.map(doc_attrs);

        tokens.append_all(quote! {
            #doc_attrs
            #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
            pub enum #type_name {
                #(#variants),*
            }

            impl #type_name {
                pub fn is_other(&self) -> bool {
                    matches!(self, Self::#other_name)
                }
            }

            impl ::std::fmt::Display for #type_name {
                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
                    f.write_str(match self {
                        #(#display_arms),*
                    })
                }
            }

            impl ::std::str::FromStr for #type_name {
                type Err = ::std::convert::Infallible;

                fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
                    ::std::result::Result::Ok(match s {
                        #(#from_str_arms),*
                    })
                }
            }

            impl<'de> ::serde::Deserialize<'de> for #type_name {
                fn deserialize<D: ::serde::Deserializer<'de>>(
                    deserializer: D,
                ) -> ::std::result::Result<Self, D::Error> {
                    struct Visitor;
                    impl<'de> ::serde::de::Visitor<'de> for Visitor {
                        type Value = #type_name;

                        fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
                            f.write_str(#expecting)
                        }

                        fn visit_str<E: ::serde::de::Error>(
                            self,
                            s: &str,
                        ) -> ::std::result::Result<Self::Value, E> {
                            let ::std::result::Result::Ok(v) = ::std::str::FromStr::from_str(s);
                            Ok(v)
                        }
                    }
                    ::serde::Deserializer::deserialize_str(deserializer, Visitor)
                }
            }

            impl ::serde::Serialize for #type_name {
                fn serialize<S: ::serde::Serializer>(
                    &self,
                    serializer: S,
                ) -> ::std::result::Result<S::Ok, S::Error> {
                    match self {
                        Self::#other_name => Err(::serde::ser::Error::custom(#other_serialize_error)),
                        v => v.to_string().serialize(serializer),
                    }
                }
            }
        })
    }
}
