use super::*;

fn enum_delegate_variant_tokens(variant: &Variant) -> Result<proc_macro2::TokenStream> {
    let ident = &variant.ident;
    Ok(match &variant.fields {
        Fields::Unit => quote!(#ident),
        Fields::Unnamed(fields) => {
            let fields = fields.unnamed.iter().map(|field| {
                let ty = &field.ty;
                quote!(<#ty as ::lumen_engine::node::Delegated>::Delegate)
            });
            quote!(#ident(#(#fields),*))
        }
        Fields::Named(fields) => {
            let fields = fields.named.iter().map(|field| {
                let ident = field.ident.as_ref().expect("named field");
                let ty = &field.ty;
                quote!(#ident: <#ty as ::lumen_engine::node::Delegated>::Delegate)
            });
            quote!(#ident { #(#fields),* })
        }
    })
}

fn enum_from_match_tokens(
    enum_ident: &Ident,
    variant: &Variant,
) -> Result<proc_macro2::TokenStream> {
    let ident = &variant.ident;
    Ok(match &variant.fields {
        Fields::Unit => quote!(#enum_ident::#ident => Self::#ident),
        Fields::Unnamed(fields) => {
            let vars = (0..fields.unnamed.len())
                .map(|index| format_ident!("field_{index}"))
                .collect::<Vec<_>>();
            quote!(
                #enum_ident::#ident(#(#vars),*) => Self::#ident(
                    #(::lumen_engine::node::Delegated::into_delegate(#vars)),*
                )
            )
        }
        Fields::Named(fields) => {
            let vars = fields
                .named
                .iter()
                .map(|field| field.ident.clone().expect("named field"))
                .collect::<Vec<_>>();
            quote!(
                #enum_ident::#ident { #(#vars),* } => Self::#ident {
                    #(#vars: ::lumen_engine::node::Delegated::into_delegate(#vars)),*
                }
            )
        }
    })
}

fn enum_eval_match_tokens(
    enum_ident: &Ident,
    variant: &Variant,
) -> Result<proc_macro2::TokenStream> {
    let ident = &variant.ident;
    Ok(match &variant.fields {
        Fields::Unit => quote!(Self::#ident => Ok(#enum_ident::#ident)),
        Fields::Unnamed(fields) => {
            let vars = (0..fields.unnamed.len())
                .map(|index| format_ident!("field_{index}"))
                .collect::<Vec<_>>();
            quote!(
                Self::#ident(#(#vars),*) => Ok(#enum_ident::#ident(
                    #(::lumen_engine::node::DelegateEvaluable::eval(#vars, ctx)?),*
                ))
            )
        }
        Fields::Named(fields) => {
            let vars = fields
                .named
                .iter()
                .map(|field| field.ident.clone().expect("named field"))
                .collect::<Vec<_>>();
            quote!(
                Self::#ident { #(#vars),* } => Ok(#enum_ident::#ident {
                    #(#vars: ::lumen_engine::node::DelegateEvaluable::eval(#vars, ctx)?),*
                })
            )
        }
    })
}

fn enum_to_int_match_tokens(variant: &Variant) -> Result<proc_macro2::TokenStream> {
    let ident = &variant.ident;
    Ok(quote!(Self::#ident => Self::#ident as i64))
}

fn enum_from_int_match_tokens(variant: &Variant) -> Result<proc_macro2::TokenStream> {
    let ident = &variant.ident;
    Ok(quote!(value if value == Self::#ident as i64 => ::core::option::Option::Some(Self::#ident)))
}

fn enum_field_types(variants: &Punctuated<Variant, Token![,]>) -> Vec<Type> {
    variants
        .iter()
        .flat_map(|variant| match &variant.fields {
            Fields::Unit => Vec::new(),
            Fields::Unnamed(fields) => fields
                .unnamed
                .iter()
                .map(|field| field.ty.clone())
                .collect::<Vec<_>>(),
            Fields::Named(fields) => fields
                .named
                .iter()
                .map(|field| field.ty.clone())
                .collect::<Vec<_>>(),
        })
        .collect()
}
pub(crate) fn expand_enum_delegate(
    input: &DeriveInput,
    variants: &Punctuated<Variant, Token![,]>,
) -> Result<proc_macro2::TokenStream> {
    let ident = &input.ident;
    let delegate = parse_delegate_attr(input)?;
    let delegate_ident = delegate.name.clone();
    let has_kind = delegate.kind.is_some();
    let is_enum_kind = delegate
        .kind
        .as_ref()
        .is_some_and(|k| k == "enum" || k == "Enum");
    let node_meta_type_impl = if let Some(kind) = &delegate.kind {
        let property_kind = property_kind_tokens(kind);
        let enum_def_method: Option<proc_macro2::TokenStream> = if is_enum_kind {
            Some(quote! {
                #[cfg(any(feature = "json", feature = "metadata"))]
                fn enum_def() -> ::core::option::Option<&'static ::lumen_engine::node::EnumDef> {
                    ::core::option::Option::Some(<#ident as ::lumen_engine::node::NodeEnum>::enum_def())
                }
            })
        } else {
            None
        };
        quote! {
            impl ::lumen_engine::node::NodeParamType for #ident {
                fn property_kind() -> ::lumen_engine::node::PropertyKind {
                    #property_kind
                }
                #enum_def_method
            }
        }
    } else {
        quote! {
            impl ::lumen_engine::node::NodeParamType for #ident {
                fn property_kind() -> ::lumen_engine::node::PropertyKind {
                    ::lumen_engine::node::PropertyKind::String
                }
            }
        }
    };
    let delegate_variants = variants
        .iter()
        .map(enum_delegate_variant_tokens)
        .collect::<Result<Vec<_>>>()?;
    let from_matches = variants
        .iter()
        .map(|variant| enum_from_match_tokens(ident, variant))
        .collect::<Result<Vec<_>>>()?;
    let eval_matches = variants
        .iter()
        .map(|variant| enum_eval_match_tokens(ident, variant))
        .collect::<Result<Vec<_>>>()?;
    let assert_field_types = enum_field_types(variants)
        .into_iter()
        .map(|ty| quote!(__lumen_delegate_assert_delegated::<#ty>();))
        .collect::<Vec<_>>();
    let deferred_value_impl = if is_enum_kind {
        let to_int_matches = variants
            .iter()
            .map(enum_to_int_match_tokens)
            .collect::<Result<Vec<_>>>()?;
        let from_int_matches = variants
            .iter()
            .map(enum_from_int_match_tokens)
            .collect::<Result<Vec<_>>>()?;
        quote! {
            impl ::lumen_engine::node::DeferredValue for #ident {
                fn eval_deferred(
                    deferred: &::lumen_engine::node::Deferred<Self>,
                    node_id: ::lumen_engine::node::NodeId,
                    property_path: &str,
                    ctx: &::lumen_engine::expr::ExpressionContext<'_>,
                ) -> ::core::result::Result<Self, ::lumen_engine::error::LumenError> {
                    match deferred {
                        ::lumen_engine::node::Deferred::Value(value) => Ok(*value),
                        ::lumen_engine::node::Deferred::Expr(expr) => {
                            let value = expr.evaluate(ctx)?.as_f64().ok_or_else(|| {
                                ::lumen_engine::node::PropertyValue::invalid_type(
                                    node_id,
                                    property_path,
                                    "Enum",
                                    "expression",
                                )
                            })? as i64;
                            Self::from_i64(value).ok_or_else(|| {
                                ::lumen_engine::node::PropertyValue::invalid_type(
                                    node_id,
                                    property_path,
                                    "Enum",
                                    "range",
                                )
                            })
                        }
                    }
                }

                fn to_property_value(value: &Self) -> ::lumen_engine::node::PropertyValue {
                    ::lumen_engine::node::PropertyValue::Int(value.to_i64())
                }

                fn from_property_value(value: ::lumen_engine::node::PropertyValue) -> ::core::option::Option<Self> {
                    value.coerce_int().and_then(Self::from_i64)
                }

                fn property_kind_name() -> &'static str {
                    "Enum"
                }
            }

            impl #ident {
                pub fn to_i64(self) -> i64 {
                    match self {
                        #(#to_int_matches,)*
                    }
                }

                pub fn from_i64(value: i64) -> ::core::option::Option<Self> {
                    match value {
                        #(#from_int_matches,)*
                        _ => ::core::option::Option::None,
                    }
                }
            }
        }
    } else {
        quote! {}
    };

    let enum_property_methods = if has_kind {
        quote! {
            fn to_property_value(&self) -> ::lumen_engine::node::PropertyValue {
                <#ident as ::lumen_engine::node::DeferredValue>::to_property_value(
                    &self
                        .clone()
                        .into_evaluated()
                        .expect("delegate enum property values cannot contain expressions"),
                )
            }

            fn from_property_expression(
                value: ::lumen_engine::node::PropertyExpression,
            ) -> ::core::result::Result<Self, ::lumen_engine::error::LumenError> {
                match value {
                    ::lumen_engine::node::PropertyExpression::Value(value) => {
                        <#ident as ::lumen_engine::node::DeferredValue>::from_property_value(value)
                            .map(::core::convert::Into::into)
                            .ok_or_else(|| ::lumen_engine::error::LumenError::Property(
                                ::lumen_engine::error::PropertyError::InvalidType {
                                    node_id: ::lumen_engine::node::NodeId::new(0),
                                    property_path: ::std::string::String::new(),
                                    expected: <#ident as ::lumen_engine::node::DeferredValue>::property_kind_name(),
                                    actual: "property",
                                }
                            ))
                    }
                    ::lumen_engine::node::PropertyExpression::Expr(_) => {
                        Err(::lumen_engine::error::LumenError::Property(
                            ::lumen_engine::error::PropertyError::InvalidType {
                                node_id: ::lumen_engine::node::NodeId::new(0),
                                property_path: ::std::string::String::new(),
                                expected: <#ident as ::lumen_engine::node::DeferredValue>::property_kind_name(),
                                actual: "expression",
                            }
                        ))
                    }
                }
            }
        }
    } else {
        quote! {
            fn to_property_value(&self) -> ::lumen_engine::node::PropertyValue {
                ::lumen_engine::node::PropertyValue::String(::std::string::String::new())
            }

            fn from_property_expression(
                _value: ::lumen_engine::node::PropertyExpression,
            ) -> ::core::result::Result<Self, ::lumen_engine::error::LumenError> {
                Ok(<Self as ::core::default::Default>::default())
            }
        }
    };

    Ok(quote! {
        #[derive(Debug, Clone)]
        #[cfg_attr(feature = "json", derive(::serde::Deserialize))]
        pub enum #delegate_ident {
            #(#delegate_variants,)*
        }

        impl ::core::convert::From<#ident> for #delegate_ident {
            fn from(value: #ident) -> Self {
                match value {
                    #(#from_matches,)*
                }
            }
        }

        impl ::core::default::Default for #delegate_ident {
            fn default() -> Self {
                <#ident as ::core::default::Default>::default().into()
            }
        }

        impl #delegate_ident {
            pub fn try_into_evaluated(
                &self,
                ctx: &::lumen_engine::node::DelegateEvalContext<'_>,
            ) -> ::core::result::Result<#ident, ::lumen_engine::error::LumenError> {
                match self {
                    #(#eval_matches,)*
                }
            }

            pub fn into_evaluated(
                self,
            ) -> ::core::result::Result<#ident, ::lumen_engine::error::LumenError> {
                let expr = ::lumen_engine::expr::ExpressionContext::default();
                let ctx = ::lumen_engine::node::DelegateEvalContext {
                    node_id: ::lumen_engine::node::NodeId::new(0),
                    property_path: "",
                    expr: &expr,
                };
                self.try_into_evaluated(&ctx)
            }
        }

        impl ::lumen_engine::node::Delegated for #ident {
            type Delegate = #delegate_ident;

            fn into_delegate(self) -> Self::Delegate {
                self.into()
            }
        }

        impl ::lumen_engine::node::DelegateEvaluable for #delegate_ident {
            type Evaluated = #ident;

            fn eval(
                &self,
                ctx: &::lumen_engine::node::DelegateEvalContext<'_>,
            ) -> ::core::result::Result<Self::Evaluated, ::lumen_engine::error::LumenError> {
                self.try_into_evaluated(ctx)
            }
        }

        impl ::lumen_engine::node::DelegateValue for #delegate_ident {
            #enum_property_methods
        }

        #node_meta_type_impl
        #deferred_value_impl

        const _: fn() = || {
            fn __lumen_delegate_assert_delegated<T: ::lumen_engine::node::Delegated>() {}
            #(#assert_field_types)*
        };
    })
}
