/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jclouds.json.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.jclouds.reflect.Reflection2.typeToken;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;

import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
import org.jclouds.json.internal.NamingStrategies.AnnotationConstructorNamingStrategy;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

/**
 * Creates type adapters for types handled in the following ways:
 * <p/>
 * <ul>
 * <li>Deserialization</li>
 * If there's an annotation designating a parameterized constructor, invoke that for fields correlating to named
 * parameter annotations. Otherwise, use {@link ConstructorConstructor}, and set fields via reflection.
 * <p/>
 * Notes: primitive constructor params are set to the Java defaults (0 or false) if not present; and the empty object
 * ({}) is treated as a null if the constructor for the object throws an NPE.
 * <li>Serialization</li> Serialize based on reflective access to fields, delegating to ReflectiveTypeAdaptor.
 * </ul>
 * <h3>Example: Using javax inject to select a constructor and corresponding named parameters</h3>
 * <p/>
 * 
 * <pre>
 * 
 * import NamingStrategies.*;
 * 
 * serializationStrategy = new AnnotationOrNameFieldNamingStrategy(
 *    new ExtractSerializedName(), new ExtractNamed());
 * 
 * deserializationStrategy = new AnnotationConstructorNamingStrategy(
 *    ImmutableSet.of(javax.inject.Inject.class),
 *    ImmutableSet.of(new ExtractNamed()));
 *    
 * factory = new DeserializationConstructorAndReflectiveTypeAdapterFactory(new ConstructorConstructor(),
 *      serializationStrategy, Excluder.DEFAULT, deserializationStrategy);
 * 
 * gson = new GsonBuilder(serializationStrategy).registerTypeAdapterFactory(factory).create();
 * 
 * </pre>
 * <p/>
 * The above would work fine on the following class, which has no gson-specific annotations:
 * <p/>
 * 
 * <pre>
 * private static class ImmutableAndVerifiedInCtor {
 *    final int foo;
 *    &#064;Named(&quot;_bar&quot;)
 *    final int bar;
 * 
 *    &#064;Inject
 *    ImmutableAndVerifiedInCtor(@Named(&quot;foo&quot;) int foo, @Named(&quot;_bar&quot;) int bar) {
 *       if (foo &lt; 0)
 *          throw new IllegalArgumentException(&quot;negative!&quot;);
 *       this.foo = foo;
 *       this.bar = bar;
 *    }
 * }
 * </pre>
 * <p/>
 * <br/>
 */
public final class DeserializationConstructorAndReflectiveTypeAdapterFactory implements TypeAdapterFactory {
   private final AnnotationConstructorNamingStrategy constructorFieldNamingPolicy;
   private final ReflectiveTypeAdapterFactory delegateFactory;

   /**
    * @see ReflectiveTypeAdapterFactory
    */
   public DeserializationConstructorAndReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
         FieldNamingStrategy serializationFieldNamingPolicy, Excluder excluder,
         AnnotationConstructorNamingStrategy deserializationFieldNamingPolicy) {
      this.constructorFieldNamingPolicy = checkNotNull(deserializationFieldNamingPolicy,
            "deserializationFieldNamingPolicy");
      this.delegateFactory = new ReflectiveTypeAdapterFactory(constructorConstructor, checkNotNull(
            serializationFieldNamingPolicy, "fieldNamingPolicy"), checkNotNull(excluder, "excluder"),
              new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor));
   }

   public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
      com.google.common.reflect.TypeToken<T> token = typeToken(type.getType());
      Invokable<T, T> deserializationTarget = constructorFieldNamingPolicy.getDeserializer(token);

      if (deserializationTarget == null) {
         return null; // allow GSON to choose the correct Adapter (can't simply return delegateFactory.create())
      }
      // @AutoValue is SOURCE retention, which means it cannot be looked up at runtime.
      // Assume abstract types built by static methods are AutoValue.
      if (Modifier.isAbstract(type.getRawType().getModifiers()) && deserializationTarget.isStatic()) {
         // Lookup the generated AutoValue class, whose fields must be read for serialization.
         String packageName = type.getRawType().getPackage().getName();
         String autoClassName = type.getRawType().getName().replace('$', '_')
               .replace(packageName + ".", packageName + ".AutoValue_");
         try {
            type = (TypeToken<T>) TypeToken.get(type.getRawType().getClassLoader().loadClass(autoClassName));
         } catch (ClassNotFoundException ignored) {
         }
      }
      return new DeserializeIntoParameterizedConstructor<T>(delegateFactory.create(gson, type), deserializationTarget,
            getParameterReaders(gson, deserializationTarget));
   }

   private static final class DeserializeIntoParameterizedConstructor<T> extends TypeAdapter<T> {
      private final TypeAdapter<T> serializer;
      private final Invokable<T, T> parameterizedCtor;
      private final Map<String, ParameterReader<?>> parameterReaders;

      private DeserializeIntoParameterizedConstructor(TypeAdapter<T> serializer, Invokable<T, T> deserializationCtor,
            Map<String, ParameterReader<?>> parameterReaders) {
         this.serializer = serializer;
         this.parameterizedCtor = deserializationCtor;
         this.parameterReaders = parameterReaders;
      }

      @Override
      public T read(JsonReader in) throws IOException {
         if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
         }

         List<Parameter> params = parameterizedCtor.getParameters();
         Object[] values = new Object[params.size()];
         boolean empty = true;

         // Set all primitive constructor params to defaults
         for (Parameter param : params) {
            if (param.getType().getRawType() == boolean.class) {
               values[param.hashCode()] = Boolean.FALSE;
            } else if (param.getType().getRawType().isPrimitive()) {
               values[param.hashCode()] = 0;
            }
         }

         try {
            in.beginObject();
            while (in.hasNext()) {
               empty = false;
               String name = in.nextName();
               ParameterReader<?> parameter = parameterReaders.get(name);
               if (parameter == null || in.peek() == JsonToken.NULL) {
                  in.skipValue();
               } else {
                  Object value = parameter.read(in);
                  if (value != null)
                     values[parameter.position] = value;
               }
            }
         } catch (IllegalStateException e) {
            throw new JsonSyntaxException(e);
         }

         for (Parameter param : params) {
            if (param.getType().getRawType().isPrimitive()) {
               checkArgument(values[param.hashCode()] != null,
                  "Primitive param[%s] in constructor %s cannot be absent!", param.hashCode(), parameterizedCtor);
            } else if (param.getType().getRawType() == Optional.class && values[param.hashCode()] == null) {
               values[param.hashCode()] = Optional.absent();
            }
         }
         in.endObject();

         try {
            return newInstance(values);
         } catch (NullPointerException ex) {
            // If {} was found and constructor threw NPE, we treat the field as null
            if (empty && values.length > 0) {
               return null;
            }
            throw ex;
         }
      }

      /**
       * pass to delegate
       */
      @Override
      public void write(JsonWriter out, T value) throws IOException {
         serializer.write(out, value);
      }

      private T newInstance(Object[] ctorParams) throws AssertionError {
         try {
            return parameterizedCtor.invoke(null, ctorParams);
         } catch (IllegalAccessException e) {
            throw new AssertionError(e);
         } catch (InvocationTargetException e) {
            if (e.getCause() instanceof RuntimeException)
               throw RuntimeException.class.cast(e.getCause());
            throw new AssertionError(e);
         }
      }

      @Override
      public int hashCode() {
         return Objects.hashCode(serializer, parameterizedCtor, parameterReaders);
      }

      @Override
      public boolean equals(Object obj) {
         if (this == obj)
            return true;
         if (obj == null || getClass() != obj.getClass())
            return false;
         DeserializeIntoParameterizedConstructor<?> that = DeserializeIntoParameterizedConstructor.class.cast(obj);
         return Objects.equal(this.serializer, that.serializer)
               && Objects.equal(this.parameterizedCtor, that.parameterizedCtor)
               && Objects.equal(this.parameterReaders, that.parameterReaders);
      }

      @Override
      public String toString() {
         return MoreObjects.toStringHelper(this).add("parameterizedCtor", parameterizedCtor)
               .add("parameterReaders", parameterReaders).add("serializer", serializer).toString();
      }

   }

   // logic borrowed from ReflectiveTypeAdapterFactory
   static class ParameterReader<T> {
      final String name;
      final int position;
      final TypeAdapter<T> typeAdapter;

      ParameterReader(int position, String name, TypeAdapter<T> typeAdapter) {
         this.name = name;
         this.position = position;
         this.typeAdapter = typeAdapter;
      }

      public Object read(JsonReader reader) throws IOException {
         return typeAdapter.read(reader);
      }

      @Override
      public boolean equals(Object obj) {
         if (obj instanceof ParameterReader) {
            ParameterReader<?> that = ParameterReader.class.cast(obj);
            return position == that.position && name.equals(that.name);
         }
         return false;
      }

      @Override
      public int hashCode() {
         return Objects.hashCode(position, name);
      }

      @Override
      public String toString() {
         return typeAdapter + " arg" + position;
      }
   }

   private <T> Map<String, ParameterReader<?>> getParameterReaders(Gson context, Invokable<T, T> deserializationCtor) {
      Builder<String, ParameterReader<?>> result = ImmutableMap.builder();
      for (Parameter param : deserializationCtor.getParameters()) {
         TypeAdapter<?> adapter = context.getAdapter(TypeToken.get(param.getType().getType()));
         String parameterName = constructorFieldNamingPolicy.translateName(deserializationCtor, param.hashCode());
         checkArgument(parameterName != null, deserializationCtor + " parameter " + 0 + " failed to be named by "
               + constructorFieldNamingPolicy);
         @SuppressWarnings({ "rawtypes", "unchecked" })
         ParameterReader<?> parameterReader = new ParameterReader(param.hashCode(), parameterName, adapter);
         result.put(parameterReader.name, parameterReader);
      }
      return result.build();
   }
}
