use heck::{ToPascalCase, ToShoutySnakeCase};
use quote::quote;
use syn::{spanned::Spanned, Ident};

use super::{Context, KeyGenerator, SchemaFlag, SchemaKey};

pub fn key_generator<'a>(
    key: &'a SchemaKey,
    flag: &SchemaFlag,
    aux_visibility: syn::Visibility,
) -> KeyGenerator<'a> {
    let flag_name = key.name.to_pascal_case();
    KeyGenerator::new(
        key,
        Context::new_with_aux(
            &flag_name,
            bitflag_token_stream(&flag_name, flag, aux_visibility),
        ),
    )
}

fn bitflag_token_stream(
    name: &str,
    flag: &SchemaFlag,
    visibility: syn::Visibility,
) -> proc_macro2::TokenStream {
    let value_idents = flag
        .values
        .iter()
        .map(|value| Ident::new(&value.nick.to_shouty_snake_case(), value.nick.span()))
        .collect::<Vec<_>>();

    let flags_arms = value_idents
        .iter()
        .zip(flag.values.iter())
        .map(|(value_ident, value)| {
            let value = value.value;
            quote! {
                const #value_ident = #value;
            }
        });

    let from_variant_arms =
        value_idents
            .iter()
            .zip(flag.values.iter())
            .map(|(value_ident, value)| {
                let nick = &value.nick;
                quote! {
                    #nick => this.insert(Self::#value_ident)
                }
            });

    let to_variant_arms =
        value_idents
            .iter()
            .zip(flag.values.iter())
            .map(|(value_ident, value)| {
                let nick = &value.nick;
                quote! {
                    if self.contains(Self::#value_ident) {
                        string_array.push(#nick)
                    }
                }
            });

    let name_pascal_case = name.to_pascal_case();
    let ident = Ident::new(&name_pascal_case, name_pascal_case.span());

    quote! {
        gio::glib::bitflags::bitflags! {
            #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
            #visibility struct #ident: u32 {
                #(#flags_arms)*
            }
        }

        impl gio::glib::StaticVariantType for #ident {
            fn static_variant_type() -> std::borrow::Cow<'static, gio::glib::VariantTy> {
                std::borrow::Cow::Borrowed(gio::glib::VariantTy::STRING_ARRAY)
            }
        }

        impl gio::glib::FromVariant for #ident {
            fn from_variant(variant: &gio::glib::Variant) -> Option<Self> {
                let mut this = Self::empty();

                for string in variant.get::<Vec<String>>()? {
                    match string.as_str() {
                        #(#from_variant_arms),*,
                        _ => return None,
                    }
                }

                Some(this)
            }
        }

        impl gio::glib::ToVariant for #ident {
            fn to_variant(&self) -> gio::glib::Variant {
                let mut string_array = Vec::new();

                #(#to_variant_arms)*

                gio::glib::ToVariant::to_variant(&string_array)
            }
        }

        impl std::convert::From<#ident> for gio::glib::Variant {
            fn from(this: #ident) -> gio::glib::Variant {
                gio::glib::ToVariant::to_variant(&this)
            }
        }
    }
}
