use quote::ToTokens;
use reflectapi_schema::*;

pub(crate) struct TokenizableType<'a> {
    pub inner: &'a Type,
}

impl<'a> TokenizableType<'a> {
    pub fn new(inner: &'a Type) -> Self {
        TokenizableType { inner }
    }
}

impl ToTokens for TokenizableType<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self.inner {
            Type::Enum(e) => {
                let tks = TokenizableEnum::new(e);
                tokens.extend(quote::quote! {
                    reflectapi::Type::Enum(#tks)
                });
            }
            Type::Struct(s) => {
                let tks = TokenizableStruct::new(s);
                tokens.extend(quote::quote! {
                    reflectapi::Type::Struct(#tks)
                });
            }
            Type::Primitive(p) => {
                let tks = TokenizablePrimitive::new(p);
                tokens.extend(quote::quote! {
                    reflectapi::Type::Primitive(#tks)
                });
            }
        }
    }
}

pub(crate) struct TokenizableTypeReference<'a> {
    pub inner: &'a TypeReference,
}

impl<'a> TokenizableTypeReference<'a> {
    pub fn new(inner: &'a TypeReference) -> Self {
        TokenizableTypeReference { inner }
    }
}

impl ToTokens for TokenizableTypeReference<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let arguments = self.inner.arguments().map(TokenizableTypeReference::new);
        tokens.extend(quote::quote! {
            reflectapi::TypeReference {
                name: #name.into(),
                arguments: vec![#(#arguments),*]
            }
        });
    }
}

pub(crate) struct TokenizableTypeParameter<'a> {
    pub inner: &'a TypeParameter,
}

impl<'a> TokenizableTypeParameter<'a> {
    pub fn new(inner: &'a TypeParameter) -> Self {
        TokenizableTypeParameter { inner }
    }
}

impl ToTokens for TokenizableTypeParameter<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let description = self.inner.description.as_str();
        tokens.extend(quote::quote! {
            reflectapi::TypeParameter {
                name: #name.into(),
                description: #description.into()
            }
        });
    }
}

pub(crate) struct TokenizableField<'a> {
    pub inner: &'a Field,
}

impl<'a> TokenizableField<'a> {
    pub fn new(inner: &'a Field) -> Self {
        TokenizableField { inner }
    }
}

impl ToTokens for TokenizableField<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let serde_name = self.inner.serde_name.as_str();
        let description = self.inner.description.as_str();
        let deprecation_note = self.inner.deprecation_note.as_ref().map_or_else(
            || quote::quote! { None },
            |d| quote::quote! { Some(#d.into()) },
        );
        let type_ref = TokenizableTypeReference::new(&self.inner.type_ref);
        let required = self.inner.required;
        let flattened = self.inner.flattened;
        let hidden = self.inner.hidden;
        let transform_callback = self.inner.transform_callback.as_str();
        let mut transform_callback_fn = quote::quote! {
            None
        };
        if !transform_callback.is_empty() {
            let transformation_fn_path =
                syn::parse_str::<proc_macro2::TokenStream>(transform_callback).unwrap();
            transform_callback_fn = quote::quote! {
                Some(#transformation_fn_path)
            };
        }
        tokens.extend(quote::quote! {
            reflectapi::Field {
                name: #name.into(),
                serde_name: #serde_name.into(),
                description: #description.into(),
                deprecation_note: #deprecation_note.into(),
                type_ref: #type_ref,
                required: #required,
                flattened: #flattened,
                hidden: #hidden,
                transform_callback: String::new(),
                transform_callback_fn: #transform_callback_fn,
            }
        });
    }
}

pub(crate) struct TokenizableVariant<'a> {
    pub inner: &'a Variant,
}

impl<'a> TokenizableVariant<'a> {
    pub fn new(inner: &'a Variant) -> Self {
        TokenizableVariant { inner }
    }
}

impl ToTokens for TokenizableVariant<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let serde_name = self.inner.serde_name.as_str();
        let description = self.inner.description.as_str();
        let fields = self.inner.fields().map(TokenizableField::new);

        let fields = match self.inner.fields {
            reflectapi_schema::Fields::Named(..) => quote::quote! {
                reflectapi::Fields::Named(vec![#(#fields),*])
            },
            reflectapi_schema::Fields::Unnamed(..) => quote::quote! {
                reflectapi::Fields::Unnamed(vec![#(#fields),*])
            },
            reflectapi_schema::Fields::None => quote::quote! {
                reflectapi::Fields::None
            },
        };
        let discriminant = self
            .inner
            .discriminant
            .as_ref()
            .map(|d| quote::quote! { Some(#d) })
            .unwrap_or_else(|| quote::quote! { None });
        let untagged = self.inner.untagged;
        tokens.extend(quote::quote! {
            reflectapi::Variant {
                name: #name.into(),
                serde_name: #serde_name.into(),
                description: #description.into(),
                fields: #fields,
                discriminant: #discriminant,
                untagged: #untagged,
            }
        });
    }
}

pub(crate) struct TokenizableRepresentation<'a> {
    pub inner: &'a Representation,
}

impl<'a> TokenizableRepresentation<'a> {
    pub fn new(inner: &'a Representation) -> Self {
        TokenizableRepresentation { inner }
    }
}

impl ToTokens for TokenizableRepresentation<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let tks = match self.inner {
            Representation::External => {
                quote::quote! {
                    reflectapi::Representation::External
                }
            }
            Representation::Internal { tag } => {
                quote::quote! {
                    reflectapi::Representation::Internal { tag: #tag.into() }
                }
            }
            Representation::Adjacent { tag, content } => {
                quote::quote! {
                    reflectapi::Representation::Adjacent { tag: #tag.into(), content: #content.into() }
                }
            }
            Representation::None => {
                quote::quote! {
                    reflectapi::Representation::None
                }
            }
        };
        tokens.extend(tks);
    }
}

pub(crate) struct TokenizableEnum<'a> {
    pub inner: &'a Enum,
}

impl<'a> TokenizableEnum<'a> {
    pub fn new(inner: &'a Enum) -> Self {
        TokenizableEnum { inner }
    }
}

impl ToTokens for TokenizableEnum<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let serde_name = self.inner.serde_name.as_str();
        let description = self.inner.description.as_str();
        let parameters = self.inner.parameters().map(TokenizableTypeParameter::new);
        let representation = TokenizableRepresentation::new(&self.inner.representation);
        let variants = self.inner.variants().map(TokenizableVariant::new);
        let codegen_config =
            TokenizableLanguageSpecificTypeCodegenConfig(&self.inner.codegen_config);
        tokens.extend(quote::quote! {
            reflectapi::Enum {
                name: #name.into(),
                serde_name: #serde_name.into(),
                description: #description.into(),
                parameters: vec![#(#parameters),*],
                representation: #representation,
                variants: vec![#(#variants),*],
                codegen_config: #codegen_config,
            }
        });
    }
}

pub(crate) struct TokenizableLanguageSpecificTypeCodegenConfig<'a>(
    &'a reflectapi_schema::LanguageSpecificTypeCodegenConfig,
);

impl ToTokens for TokenizableLanguageSpecificTypeCodegenConfig<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let rust = TokenizableRustTypeCodegenConfig(&self.0.rust);
        tokens.extend(quote::quote! {
            reflectapi::LanguageSpecificTypeCodegenConfig {
                rust: #rust,
            }
        });
    }
}

pub(crate) struct TokenizableRustTypeCodegenConfig<'a>(&'a RustTypeCodegenConfig);

impl ToTokens for TokenizableRustTypeCodegenConfig<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let additional_derives = &self.0.additional_derives;
        tokens.extend(quote::quote! {
            reflectapi::RustTypeCodegenConfig {
                additional_derives: std::collections::BTreeSet::from_iter([#(String::from(#additional_derives)),*]),
            }
        });
    }
}

pub(crate) struct TokenizableStruct<'a> {
    pub inner: &'a Struct,
}

impl<'a> TokenizableStruct<'a> {
    pub fn new(inner: &'a Struct) -> Self {
        TokenizableStruct { inner }
    }
}

impl ToTokens for TokenizableStruct<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let serde_name = self.inner.serde_name.as_str();
        let description = self.inner.description.as_str();
        let parameters = self.inner.parameters().map(TokenizableTypeParameter::new);
        let fields = self.inner.fields().map(TokenizableField::new);
        let fields = match self.inner.fields {
            reflectapi_schema::Fields::Named(..) => quote::quote! {
                reflectapi::Fields::Named(vec![#(#fields),*])
            },
            reflectapi_schema::Fields::Unnamed(..) => quote::quote! {
                reflectapi::Fields::Unnamed(vec![#(#fields),*])
            },
            reflectapi_schema::Fields::None => quote::quote! {
                reflectapi::Fields::None
            },
        };

        let transparent = self.inner.transparent;
        let codegen_config =
            TokenizableLanguageSpecificTypeCodegenConfig(&self.inner.codegen_config);
        tokens.extend(quote::quote! {
            reflectapi::Struct {
                name: #name.into(),
                serde_name: #serde_name.into(),
                description: #description.into(),
                parameters: vec![#(#parameters),*],
                fields: #fields,
                transparent: #transparent,
                codegen_config: #codegen_config,
            }
        });
    }
}

pub(crate) struct TokenizablePrimitive<'a> {
    pub inner: &'a Primitive,
}

impl<'a> TokenizablePrimitive<'a> {
    pub fn new(inner: &'a Primitive) -> Self {
        TokenizablePrimitive { inner }
    }
}

impl ToTokens for TokenizablePrimitive<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let name = self.inner.name.as_str();
        let description = self.inner.description.as_str();
        let parameters = self.inner.parameters().map(TokenizableTypeParameter::new);
        let fallback = self
            .inner
            .fallback
            .as_ref()
            .map(TokenizableTypeReference::new);
        let codegen_config =
            TokenizableLanguageSpecificTypeCodegenConfig(&self.inner.codegen_config);
        tokens.extend(quote::quote! {
            reflectapi::Primitive {
                name: #name.into(),
                description: #description.into(),
                parameters: vec![#(#parameters),*],
                fallback: #fallback,
                codegen_config: #codegen_config,
            }
        });
    }
}
