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

import com.google.gson.FieldNamingStrategy;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.TypeAdapterFactory;
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.ScalarDeserializationStrategy;
import io.leangen.graphql.metadata.strategy.value.ValueMapper;
import io.leangen.graphql.metadata.strategy.value.ValueMapperFactory;
import io.leangen.graphql.util.ClassUtils;
import net.dongliu.gson.GsonJava8TypeAdapterFactory;

import java.lang.reflect.AnnotatedType;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @author Bojan Tomic (kaqqao)
 */
public class GsonValueMapperFactory implements ValueMapperFactory<GsonValueMapper>, ScalarDeserializationStrategy {

    private final FieldNamingStrategy fieldNamingStrategy;
    private final TypeInfoGenerator typeInfoGenerator;
    private final Configurer configurer;

    @SuppressWarnings("WeakerAccess")
    public GsonValueMapperFactory() {
        this(new DefaultTypeInfoGenerator(), new GsonFieldNamingStrategy(), new AbstractClassAdapterConfigurer());
    }

    private GsonValueMapperFactory(TypeInfoGenerator typeInfoGenerator, FieldNamingStrategy fieldNamingStrategy, Configurer configurer) {
        this.fieldNamingStrategy = Objects.requireNonNull(fieldNamingStrategy);
        this.typeInfoGenerator = Objects.requireNonNull(typeInfoGenerator);
        this.configurer = Objects.requireNonNull(configurer);
    }

    @Override
    public GsonValueMapper getValueMapper(Map<Class, List<Class>> concreteSubTypes, GlobalEnvironment environment) {
        return new GsonValueMapper(initBuilder(concreteSubTypes, environment).create());
    }

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

    private GsonBuilder initBuilder(Map<Class, List<Class>> concreteSubTypes, GlobalEnvironment environment) {
        GsonBuilder gsonBuilder = new GsonBuilder()
                .setFieldNamingStrategy(fieldNamingStrategy)
                .registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory());
        return configurer.configure(gsonBuilder, concreteSubTypes, this.typeInfoGenerator, environment);
    }

    @Override
    public boolean isDirectlyDeserializable(AnnotatedType type) {
        return GenericTypeReflector.isSuperType(JsonElement.class, type.getType());
    }

    public static class AbstractClassAdapterConfigurer implements Configurer {

        @Override
        public GsonBuilder configure(GsonBuilder gsonBuilder, Map<Class, List<Class>> concreteSubTypes, TypeInfoGenerator infoGenerator, GlobalEnvironment environment) {
            concreteSubTypes.entrySet().stream()
                    .map(entry -> adapterFor(entry.getKey(), entry.getValue(), infoGenerator))
                    .filter(Objects::nonNull)
                    .forEach(gsonBuilder::registerTypeAdapterFactory);

            if (environment != null && !environment.getInputConverters().isEmpty()) {
                gsonBuilder.registerTypeAdapterFactory(new ConvertingAdapterFactory(environment));
            }

            return gsonBuilder;
        }

        @SuppressWarnings("unchecked")
        private TypeAdapterFactory adapterFor(Class superClass, List<Class> implementations, TypeInfoGenerator infoGen) {
            RuntimeTypeAdapterFactory adapterFactory = RuntimeTypeAdapterFactory.of(superClass, ValueMapper.TYPE_METADATA_FIELD_NAME);
            if (implementations.isEmpty()) {
                return null;
            }
            implementations.stream()
                    .filter(impl -> !ClassUtils.isAbstract(impl))
                    .forEach(impl -> adapterFactory.registerSubtype(impl, infoGen.generateTypeName(GenericTypeReflector.annotate(impl))));

            return adapterFactory;
        }
    }

    @FunctionalInterface
    public interface Configurer {
        GsonBuilder configure(GsonBuilder gsonBuilder, Map<Class, List<Class>> concreteSubTypes, TypeInfoGenerator infoGenerator, GlobalEnvironment environment);
    }

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

    public static class Builder {

        private FieldNamingStrategy fieldNamingStrategy = new GsonFieldNamingStrategy();
        private TypeInfoGenerator typeInfoGenerator = new DefaultTypeInfoGenerator();
        private Configurer configurer = new AbstractClassAdapterConfigurer();

        public Builder withFieldNamingStrategy(FieldNamingStrategy fieldNamingStrategy) {
            this.fieldNamingStrategy = fieldNamingStrategy;
            return this;
        }

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

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

        public GsonValueMapperFactory build() {
            return new GsonValueMapperFactory(typeInfoGenerator, fieldNamingStrategy, configurer);
        }
    }
}

@@ -91,24 +91,53 @@
 ngStrategy;%0A
+import com.google.gson.Gson;%0A
 import com.g
@@ -1063,24 +1063,58 @@
 Strategy %7B%0A%0A
+    private final Gson prototype;%0A
     private 
@@ -1346,16 +1346,22 @@
    this(
+null, 
 new Defa
@@ -1496,16 +1496,32 @@
 Factory(
+Gson prototype, 
 TypeInfo
@@ -1607,32 +1607,68 @@
 r configurer) %7B%0A
+        this.prototype = prototype;%0A
         this.fie
@@ -2318,24 +2318,70 @@
 sonBuilder =
+ (prototype != null ? prototype.newBuilder() :
  new GsonBui
@@ -2386,16 +2386,17 @@
 uilder()
+)
 %0A       
@@ -4681,24 +4681,56 @@
  Builder %7B%0A%0A
+        private Gson prototype;%0A
         priv
@@ -5325,32 +5325,163 @@
 his;%0A        %7D%0A%0A
+        public Builder withPrototype(Gson prototype) %7B%0A            this.prototype = prototype;%0A            return this;%0A        %7D%0A%0A
         public B
@@ -5699,16 +5699,27 @@
 Factory(
+prototype, 
 typeInfo

Configure GsonValueMapperFactory using a prototype Gson instance