package io.leangen.graphql.generator.mapping.common;

import graphql.schema.GraphQLOutputType;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.annotations.types.GraphQLUnion;
import io.leangen.graphql.generator.BuildContext;
import io.leangen.graphql.generator.mapping.TypeMapper;
import io.leangen.graphql.generator.mapping.TypeMappingEnvironment;
import io.leangen.graphql.metadata.exceptions.TypeMappingException;
import io.leangen.graphql.util.ClassUtils;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Bojan Tomic (kaqqao)
 */
public class UnionTypeMapper extends UnionMapper {

    @Override
    public GraphQLOutputType toGraphQLType(AnnotatedType javaType, Set<Class<? extends TypeMapper>> mappersToSkip, TypeMappingEnvironment env) {
        GraphQLUnion annotation = javaType.getAnnotation(GraphQLUnion.class);
        List<AnnotatedType> possibleJavaTypes = getPossibleJavaTypes(javaType, env.buildContext);
        final String name = env.buildContext.typeInfoGenerator.generateTypeName(javaType, env.buildContext.messageBundle);
        return toGraphQLUnion(name, annotation.description(), javaType, possibleJavaTypes, env);
    }

    @Override
    public boolean supports(AnnotatedElement element, AnnotatedType type) {
        return type.isAnnotationPresent(GraphQLUnion.class)
                || ClassUtils.getRawType(type.getType()).isAnnotationPresent(GraphQLUnion.class);
    }

    private List<AnnotatedType> getPossibleJavaTypes(AnnotatedType javaType, BuildContext buildContext) {
        GraphQLUnion annotation = javaType.getAnnotation(GraphQLUnion.class);
        List<AnnotatedType> possibleTypes = Collections.emptyList();
        if (annotation.possibleTypes().length > 0) {
            possibleTypes = Arrays.stream(annotation.possibleTypes())
                    .map(type -> GenericTypeReflector.getExactSubType(javaType, type))
                    .collect(Collectors.toList());
        }
        if (possibleTypes.isEmpty()) {
            try {
                possibleTypes = annotation.possibleTypeFactory().newInstance().getPossibleTypes();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new IllegalArgumentException(annotation.possibleTypeFactory().getName() +
                        " must have a public default constructor", e);
            }
        }
        if (possibleTypes.isEmpty()) {
            possibleTypes = buildContext.implDiscoveryStrategy.findImplementations(javaType, annotation.possibleTypeAutoDiscovery(), annotation.scanPackages(), buildContext);
        }
        if (possibleTypes.isEmpty()) {
            throw new TypeMappingException("No possible types found for union type " + javaType.getType().getTypeName());
        }
        return possibleTypes;
    }
}