use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, Data, Fields};

use crate::util;

pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStream {
    let enum_fields = match ast.data {
        Data::Enum(data) => data.variants,
        _ => {
            panic!("#[derive(GraphQLUnion)] can only be applied to enums");
        }
    };

    // Parse attributes.
    let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) {
        Ok(a) => a,
        Err(e) => {
            panic!("Invalid #[graphql(...)] attribute for enum: {}", e);
        }
    };

    if !attrs.interfaces.is_empty() {
        panic!("#[derive(GraphQLUnion)] does not support interfaces");
    }

    let ident = &ast.ident;
    let name = attrs.name.unwrap_or_else(|| ident.to_string());

    let fields = enum_fields.into_iter().filter_map(|field| {
        let field_attrs = match util::FieldAttributes::from_attrs(
            field.attrs,
            util::FieldAttributeParseMode::Object,
        ) {
            Ok(attrs) => attrs,
            Err(e) => panic!("Invalid #[graphql] attribute for field: \n{}", e),
        };


        if field_attrs.skip {
            panic!("#[derive(GraphQLUnion)] does not support #[graphql(skip)] on fields");
        } else {
            let variant_name = field.ident;
            let name = field_attrs
                .name
                .clone()
                .unwrap_or_else(|| util::to_camel_case(&variant_name.to_string()));

            let resolver_code = quote!(
                #ident :: #variant_name
            );

            let _type = match field.fields {
                Fields::Unnamed(inner) => {
                    let mut iter = inner.unnamed.iter();
                    let first = match iter.next() {
                        Some(val) => val,
                        None => unreachable!(),
                    };

                    if iter.next().is_some() {
                        panic!("#[derive(GraphQLUnion)] all members must be unnamed with a single element e.g. Some(T)");
                    }

                    first.ty.clone()
                }
                _ => panic!("#[derive(GraphQLUnion)] all fields of the enum must be unnamed"),
            };

            if field_attrs.description.is_some() {
                panic!("#[derive(GraphQLUnion)] does not allow documentation of fields");
            }

            Some(util::GraphQLTypeDefinitionField {
                name,
                _type,
                args: Vec::new(),
                description: None,
                deprecation: field_attrs.deprecation,
                resolver_code,
                is_type_inferred: true,
                is_async: false,
            })
        }
    });

    let fields = fields.collect::<Vec<_>>();

    // NOTICE: This is not an optimal implementation. It is possible
    // to bypass this check by using a full qualified path instead
    // (crate::Test vs Test). Since this requirement is mandatory, the
    // `std::convert::Into<T>` implementation is used to enforce this
    // requirement. However, due to the bad error message this
    // implementation should stay and provide guidance.
    let all_variants_different = {
        let mut all_types: Vec<_> = fields.iter().map(|field| &field._type).collect();
        let before = all_types.len();
        all_types.dedup();
        before == all_types.len()
    };

    if !all_variants_different {
        panic!("#[derive(GraphQLUnion)] each variant must have a different type");
    }

    let definition = util::GraphQLTypeDefiniton {
        name,
        _type: syn::parse_str(&ast.ident.to_string()).unwrap(),
        context: attrs.context,
        scalar: attrs.scalar,
        description: attrs.description,
        fields,
        generics: ast.generics,
        interfaces: None,
        include_type_generics: true,
        generic_scalar: true,
        no_async: attrs.no_async,
    };

    let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
    definition.into_union_tokens(juniper_crate_name)
}