use proc_macro2 as m4;
use quote::quote;

use super::utilities::{
    entity_name, entity_union_name, reader_name, reader_union_name, union_item_name, usize_lit,
};
use crate::ast::{self as ast, HasName};

pub(in super::super) trait GenEnumerator {
    fn gen_enumerator(&self) -> m4::TokenStream;
}

impl GenEnumerator for ast::Union {
    #[allow(clippy::cognitive_complexity)]
    fn gen_enumerator(&self) -> m4::TokenStream {
        let entity_union = entity_union_name(self.name());
        let reader_union = reader_union_name(self.name());
        let entity_union_string = entity_union.to_string();
        let reader_union_string = reader_union.to_string();
        let inner_len = self.items().len();
        let (
            ref entity_inners,
            ref reader_inners,
            ref union_items,
            ref union_ids,
            ref entity_union_item_paths,
            ref reader_union_item_paths,
        ) = {
            self.items().iter().fold(
                (
                    Vec::with_capacity(inner_len),
                    Vec::with_capacity(inner_len),
                    Vec::with_capacity(inner_len),
                    Vec::with_capacity(inner_len),
                    Vec::with_capacity(inner_len),
                    Vec::with_capacity(inner_len),
                ),
                |(
                    mut entity_inners,
                    mut reader_inners,
                    mut union_items,
                    mut union_ids,
                    mut entity_union_item_paths,
                    mut reader_union_item_paths,
                ),
                 item| {
                    let inner_name = item.typ().name();
                    let entity_name = entity_name(inner_name);
                    let reader_name = reader_name(inner_name);
                    let item_name = union_item_name(inner_name);
                    let item_id = usize_lit(item.id());
                    let entity_union_item_path = quote!(#entity_union::#item_name);
                    let reader_union_item_path = quote!(#reader_union::#item_name);
                    entity_inners.push(entity_name);
                    reader_inners.push(reader_name);
                    union_items.push(item_name);
                    union_ids.push(item_id);
                    entity_union_item_paths.push(entity_union_item_path);
                    reader_union_item_paths.push(reader_union_item_path);
                    (
                        entity_inners,
                        reader_inners,
                        union_items,
                        union_ids,
                        entity_union_item_paths,
                        reader_union_item_paths,
                    )
                },
            )
        };
        let union_items_string = &union_items
            .iter()
            .map(|x| x.to_string())
            .collect::<Vec<_>>();
        let entity_default = {
            let inner = &self.items()[0];
            let item_name = union_item_name(inner.typ().name());
            quote!(#item_name(::core::default::Default::default()))
        };
        let code_union_definitions_and_impl_traits = quote!(
            #[derive(Debug, Clone)]
            pub enum #entity_union {
                #( #union_items(#entity_inners), )*
            }
            #[derive(Debug, Clone, Copy)]
            pub enum #reader_union<'r> {
                #( #union_items(#reader_inners<'r>), )*
            }

            impl ::core::default::Default for #entity_union {
                fn default() -> Self {
                    #entity_union::#entity_default
                }
            }

            impl ::core::fmt::Display for #entity_union {
                fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
                    match self {
                        #(
                            #entity_union_item_paths(ref item) => {
                                write!(f, "{}::{}({})", Self::NAME, #union_items::NAME, item)
                            }
                        )*
                    }
                }
            }
            impl<'r> ::core::fmt::Display for #reader_union<'r> {
                fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
                    match self {
                        #(
                            #reader_union_item_paths(ref item) => {
                                write!(f, "{}::{}({})", Self::NAME, #union_items::NAME, item)
                            }
                        )*
                    }
                }
            }

            impl #entity_union {
                pub(crate) fn display_inner(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
                    match self {
                        #( #entity_union_item_paths(ref item) => write!(f, "{}", item), )*
                    }
                }
            }
            impl<'r> #reader_union<'r> {
                pub(crate) fn display_inner(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
                    match self {
                        #( #reader_union_item_paths(ref item) => write!(f, "{}", item), )*
                    }
                }
            }
        );
        let code_entity_item_into_union = union_items
            .iter()
            .zip(entity_inners.iter())
            .map(|(item_name, entity_name)| {
                quote!(
                    impl ::core::convert::From<#entity_name> for #entity_union {
                        fn from(item: #entity_name) -> Self {
                            #entity_union::#item_name(item)
                        }
                    }
                )
            })
            .collect::<Vec<_>>();
        let code_reader_item_into_union = union_items
            .iter()
            .zip(reader_inners.iter())
            .map(|(item_name, reader_name)| {
                quote!(
                    impl<'r> ::core::convert::From<#reader_name<'r>> for #reader_union<'r> {
                        fn from(item: #reader_name<'r>) -> Self {
                            #reader_union::#item_name(item)
                        }
                    }
                )
            })
            .collect::<Vec<_>>();
        let code_impl_entity_union = {
            quote!(
                impl #entity_union {
                    pub const NAME: &'static str = #entity_union_string;
                    pub fn as_bytes(&self) -> molecule::bytes::Bytes {
                        match self {
                            #( #entity_union_item_paths(item) => item.as_bytes(), )*
                        }
                    }
                    pub fn as_slice(&self) -> &[u8] {
                        match self {
                            #( #entity_union_item_paths(item) => item.as_slice(), )*
                        }
                    }
                    pub fn item_id(&self) -> molecule::Number {
                        match self {
                            #( #entity_union_item_paths(_) => #union_ids, )*
                        }
                    }
                    pub fn item_name(&self) -> &str {
                        match self {
                            #( #entity_union_item_paths(_) => #union_items_string, )*
                        }
                    }
                    pub fn as_reader<'r>(&'r self) -> #reader_union<'r> {
                        match self {
                            #( #entity_union_item_paths(item) => item.as_reader().into(), )*
                        }
                    }
                }
            )
        };
        let code_impl_reader_union = {
            quote!(
                impl<'r> #reader_union<'r> {
                    pub const NAME: &'r str = #reader_union_string;
                    pub fn as_slice(&self) -> &'r [u8] {
                        match self {
                            #( #reader_union_item_paths(item) => item.as_slice(), )*
                        }
                    }
                    pub fn item_id(&self) -> molecule::Number {
                        match self {
                            #( #reader_union_item_paths(_) => #union_ids, )*
                        }
                    }
                    pub fn item_name(&self) -> &str {
                        match self {
                            #( #reader_union_item_paths(_) => #union_items_string, )*
                        }
                    }
                }
            )
        };
        quote!(
            #code_union_definitions_and_impl_traits
            #( #code_entity_item_into_union )*
            #( #code_reader_item_into_union )*
            #code_impl_entity_union
            #code_impl_reader_union
        )
    }
}
