use proc_macro::TokenStream;
use quote::quote;
use syn::{
    Data, DeriveInput, Error, Fields, Lit, Meta, MetaList, MetaNameValue, NestedMeta,
    parse_macro_input,
};

#[proc_macro_derive(Hessian, attributes(hessian))]
pub fn derive_hessian_object(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    match expand(input) {
        Ok(ts) => ts.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let name = &input.ident;

    let class_name = extract_class(&input)?;

    let named_fields = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(f) => &f.named,
            _ => {
                return Err(Error::new_spanned(
                    name,
                    "Hessian only supports named-field structs",
                ));
            }
        },
        _ => {
            return Err(Error::new_spanned(name, "Hessian only supports structs"));
        }
    };

    let mut java_names: Vec<String> = Vec::new();
    let mut rust_idents: Vec<&syn::Ident> = Vec::new();
    let mut field_types: Vec<&syn::Type> = Vec::new();

    for field in named_fields {
        let ident = field.ident.as_ref().unwrap();
        let java = extract_rename(&field.attrs)?.unwrap_or_else(|| ident.to_string());
        java_names.push(java);
        rust_idents.push(ident);
        field_types.push(&field.ty);
    }

    let field_serializers = rust_idents.iter().map(|ident| {
        quote! {
            ::hessian2::HessianSerialize::hessian_serialize(&self.#ident, w)?;
        }
    });

    Ok(quote! {
        impl ::hessian2::HessianSerialize for #name {
            fn hessian_serialize<W: ::std::io::Write>(
                &self,
                w: &mut ::hessian2::codec::Encoder<W>,
            ) -> ::hessian2::Result<()> {
                w.begin_object(
                    #class_name,
                    &[#(#java_names),*],
                )?;
                #(#field_serializers)*
                ::hessian2::Result::Ok(())
            }
        }

        impl ::hessian2::HessianDeserialize for #name {
            fn hessian_deserialize<__R: ::std::io::Read>(
                de: &mut ::hessian2::de::Deserializer<__R>,
            ) -> ::hessian2::Result<Self> {
                let mut obj = de.begin_object()?;
                #(
                    let mut #rust_idents: ::std::option::Option<#field_types> =
                        ::std::option::Option::None;
                )*
                while let ::std::option::Option::Some(field) = obj.next_field() {
                    match ::std::convert::AsRef::<str>::as_ref(&field) {
                        #(
                            #java_names => {
                                #rust_idents = ::std::option::Option::Some(obj.value()?);
                            }
                        )*
                        _ => {
                            obj.skip_value()?;
                        }
                    }
                }
                ::hessian2::Result::Ok(Self {
                    #(
                        #rust_idents: #rust_idents.ok_or_else(|| {
                            ::hessian2::Error::IO(::std::io::Error::new(
                                ::std::io::ErrorKind::InvalidData,
                                ::std::concat!("missing field `", #java_names, "`"),
                            ))
                        })?,
                    )*
                })
            }
        }
    })
}

fn extract_class(input: &DeriveInput) -> syn::Result<String> {
    for attr in &input.attrs {
        if attr.path.is_ident("hessian") {
            if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
                for item in &nested {
                    if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                        path,
                        lit: Lit::Str(s),
                        ..
                    })) = item
                    {
                        if path.is_ident("class") {
                            return Ok(s.value());
                        }
                    }
                }
            }
        }
    }
    Err(Error::new_spanned(
        &input.ident,
        "Hessian requires #[hessian(class = \"...\")]",
    ))
}

fn extract_rename(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> {
    for attr in attrs {
        if attr.path.is_ident("hessian") {
            if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
                for item in &nested {
                    if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                        path,
                        lit: Lit::Str(s),
                        ..
                    })) = item
                    {
                        if path.is_ident("rename") {
                            return Ok(Some(s.value()));
                        }
                    }
                }
            }
        }
    }
    Ok(None)
}
