use crate::attrs::SiocInput;
use darling::FromDeriveInput;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

pub fn expand(input: syn::DeriveInput) -> darling::Result<TokenStream> {
    let input = SiocInput::from_derive_input(&input)?;
    let enum_ident = &input.ident;

    let variants = match input.data {
        darling::ast::Data::Enum(v) => v,
        darling::ast::Data::Struct(..) => {
            return Err(
                darling::Error::unsupported_shape_with_expected("struct", &"enum")
                    .with_span(&input.ident),
            );
        }
    };

    let shape = darling::util::ShapeSet::new([darling::util::Shape::Newtype]);

    for variant in &variants {
        shape
            .check(&variant.fields)
            .map_err(|e| e.with_span(&variant.ident))?;
    }

    let helper_ident = format_ident!("__{enum_ident}Helper");
    let visitor_ident = format_ident!("__{enum_ident}Visitor");

    let variants = variants
        .into_iter()
        .map(|v| {
            let ident = v.ident;
            let ty = v.fields.into_iter().next().unwrap().ty;
            (ident, ty)
        })
        .collect::<Vec<_>>();

    let helper_variants = variants.iter().map(|(vi, ty)| {
        quote! { #vi(<#ty as ::sioc::prelude::EventHandler>::Payload) }
    });

    let visit_arms = variants.iter().map(|(vi, ty)| {
        quote! {
            <#ty as ::sioc::prelude::EventHandler>::Payload::NAME => Self::Value::#vi(
                <#ty as ::sioc::prelude::EventHandler>::Payload::deserialize_payload(&mut seq)?,
            ),
        }
    });

    let all_names = variants
        .iter()
        .map(|(_, ty)| quote! { <#ty as ::sioc::prelude::EventHandler>::Payload::NAME });

    let name_arms = variants.iter().map(|(vi, ty)| {
        quote! {
            #enum_ident::#vi(_) => <#ty as ::sioc::prelude::EventHandler>::Payload::NAME,
        }
    });

    let from_event_arms = variants.iter().map(|(vi, ty)| {
        quote! {
            #helper_ident::#vi(args) => {
                #enum_ident::#vi(<#ty as ::sioc::prelude::EventHandler>::handle(
                    args,
                    event.id,
                    event.attachments,
                )?)
            }
        }
    });

    Ok(quote! {
        enum #helper_ident {
            #(#helper_variants),*
        }

        impl<'de> ::serde::Deserialize<'de> for #helper_ident {
            fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
            where
                D: ::serde::Deserializer<'de>,
            {
                deserializer.deserialize_seq(#visitor_ident)
            }
        }

        struct #visitor_ident;

        impl<'de> ::serde::de::Visitor<'de> for #visitor_ident {
            type Value = #helper_ident;

            fn expecting(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
                f.write_str("a Socket.IO event payload")
            }

            fn visit_seq<V>(self, mut seq: V) -> ::std::result::Result<Self::Value, V::Error>
            where
                V: ::serde::de::SeqAccess<'de>,
            {
                let name: &str = seq
                    .next_element()?
                    .ok_or_else(|| ::serde::de::Error::invalid_length(0, &"event name"))?;

                Ok(match name {
                    #(#visit_arms)*
                    _ => {
                        return Err(::serde::de::Error::unknown_variant(name, &[#(#all_names),*]));
                    }
                })
            }
        }

        impl ::sioc::prelude::EventRouter for #enum_ident {
            fn name(&self) -> &'static str {
                match self {
                    #(#name_arms)*
                }
            }
        }

        impl ::std::convert::TryFrom<::sioc::prelude::DynEvent> for #enum_ident {
            type Error = ::sioc::error::EventError;

            fn try_from(event: ::sioc::prelude::DynEvent) -> ::std::result::Result<Self, Self::Error> {
                let helper = ::sioc::payload::from_json::<#helper_ident>(&event.payload)?;

                Ok(match helper {
                    #(#from_event_arms)*
                })
            }
        }
    })
}
