package io.leangen.graphql.metadata.strategy.value.jackson;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.NamedType;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.execution.GlobalEnvironment;
import io.leangen.graphql.metadata.strategy.type.DefaultTypeInfoGenerator;
import io.leangen.graphql.metadata.strategy.type.TypeInfoGenerator;
import io.leangen.graphql.metadata.strategy.value.ValueMapperFactory;
import io.leangen.graphql.util.ClassUtils;

/**
 * @author Bojan Tomic (kaqqao)
 */
public class JacksonValueMapperFactory implements ValueMapperFactory<JacksonValueMapper> {

    private final String[] basePackages;
    private final ObjectMapper prototype;
    private final Configurer configurer;
    private final TypeInfoGenerator typeInfoGenerator;

    public JacksonValueMapperFactory() {
        this(new String[0]);
    }

    public JacksonValueMapperFactory(String... basePackages) {
        this(basePackages, null, new DefaultTypeInfoGenerator(), new AbstractClassAdapterConfigurer());
    }

    private JacksonValueMapperFactory(String[] basePackages, ObjectMapper prototype, TypeInfoGenerator typeInfoGenerator, Configurer configurer) {
        this.basePackages = basePackages;
        this.prototype = prototype;
        this.configurer = Objects.requireNonNull(configurer);
        this.typeInfoGenerator = Objects.requireNonNull(typeInfoGenerator);
    }

    @Override
    public JacksonValueMapper getValueMapper(Set<Type> abstractTypes, GlobalEnvironment environment) {
        ObjectMapper mapper = prototype != null ? prototype.copy() : new ObjectMapper();
        ObjectMapper objectMapper = this.configurer.configure(mapper, abstractTypes, basePackages, this.typeInfoGenerator, environment);
        return new JacksonValueMapper(objectMapper);
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class AbstractClassAdapterConfigurer implements Configurer {

        @Override
        public ObjectMapper configure(ObjectMapper objectMapper, Set<Type> abstractTypes, String[] basePackages, TypeInfoGenerator metaDataGen, GlobalEnvironment environment) {
            ObjectMapper mapper = objectMapper
                    .findAndRegisterModules()
                    .setAnnotationIntrospector(new AnnotationIntrospector(collectSubtypes(abstractTypes, basePackages, metaDataGen)));
            if (environment != null && !environment.getInputConverters().isEmpty()) {
                mapper.registerModule(getDeserializersModule(environment));
            }
            return mapper;
        }

        private Map<Type, List<NamedType>> collectSubtypes(Set<Type> abstractTypes, String[] basePackages, TypeInfoGenerator metaDataGen) {
            Map<Type, List<NamedType>> types = new HashMap<>();
            Set<Class<?>> abstractClasses = abstractTypes.stream()
                    .map(ClassUtils::getRawType)
                    .distinct()
                    .collect(Collectors.toSet());
            for (Class abstractClass : abstractClasses) {
                List<NamedType> subTypes = ClassUtils.findImplementations(abstractClass, basePackages).stream()
                        .filter(impl -> !ClassUtils.isAbstract(impl))
                        .map(sub -> new NamedType(sub, metaDataGen.generateTypeName(GenericTypeReflector.annotate(sub))))
                        .collect(Collectors.toList());
                types.put(abstractClass, subTypes);
            }
            return types;
        }

        private Module getDeserializersModule(GlobalEnvironment environment) {
            return new Module() {
                @Override
                public String getModuleName() {
                    return "graphql-spqr-deserializers";
                }

                @Override
                public Version version() {
                    return Version.unknownVersion();
                }

                @Override
                public void setupModule(SetupContext setupContext) {
                    setupContext.addDeserializers(new ConvertingDeserializers(environment));
                }
            };
        }
    }

    @FunctionalInterface
    public interface Configurer {
        ObjectMapper configure(ObjectMapper objectMapper, Set<Type> abstractTypes, String[] basePackages, TypeInfoGenerator metaDataGen, GlobalEnvironment environment);
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + " with " + typeInfoGenerator.getClass().getSimpleName();
    }

    public static class Builder {

        private String[] basePackages;
        private Configurer configurer = new AbstractClassAdapterConfigurer();
        private TypeInfoGenerator typeInfoGenerator = new DefaultTypeInfoGenerator();
        private ObjectMapper prototype;

        public Builder withBasePackages(String... basePackages) {
            this.basePackages = basePackages;
            return this;
        }

        public Builder withConfigurer(Configurer configurer) {
            this.configurer = configurer;
            return this;
        }

        public Builder withTypeInfoGenerator(TypeInfoGenerator typeInfoGenerator) {
            this.typeInfoGenerator = typeInfoGenerator;
            return this;
        }

        public Builder withPrototype(ObjectMapper prototype) {
            this.prototype = prototype;
            return this;
        }

        public JacksonValueMapperFactory build() {
            return new JacksonValueMapperFactory(basePackages, prototype, typeInfoGenerator, configurer);
        }
    }
}

@@ -257,195 +257,66 @@
 pe;%0A
-%0A
 import 
-java.lang.reflect.Type;%0Aimport java.util.HashMap;%0Aimport java.util.List;%0Aimport java.util.Map;%0Aimport java.util.Objects;%0Aimport java.util.Set;%0Aimport java.util.stream.Collectors;%0A
+com.fasterxml.jackson.databind.module.SimpleModule;
 %0Aimp
@@ -674,16 +674,203 @@
 Utils;%0A%0A
+import java.lang.reflect.Type;%0Aimport java.util.HashMap;%0Aimport java.util.List;%0Aimport java.util.Map;%0Aimport java.util.Objects;%0Aimport java.util.Set;%0Aimport java.util.stream.Collectors;%0A%0A
 /**%0A * @
@@ -2665,38 +2665,26 @@
    .
-setAnnotationIntrospector(new 
+registerModule(get
 Anno
@@ -2701,16 +2701,22 @@
 ospector
+Module
 (collect
@@ -4535,24 +4535,472 @@
 %7D;%0A        %7D
+%0A%0A        private Module getAnnotationIntrospectorModule(Map%3CType, List%3CNamedType%3E%3E typeMap) %7B%0A            return new SimpleModule(%22graphql-spqr-annotation-introspector%22) %7B%0A                @Override%0A                public void setupModule(SetupContext context) %7B%0A                    super.setupModule(context);%0A                    context.insertAnnotationIntrospector(new AnnotationIntrospector(typeMap));%0A                %7D%0A            %7D;%0A        %7D
 %0A    %7D%0A%0A    

Add the custom AnnotationIntrospector instead of overriding the current