/*
 * Copyright 2008 Google Inc.
 *
 * Licensed 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 com.google.gwt.user.rebind.rpc;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GwtScriptOnly;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.CachedGeneratorResult;
import com.google.gwt.core.ext.ConfigurationProperty;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.log.speedtracer.CompilerEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.client.rpc.impl.SerializerBase;
import com.google.gwt.user.client.rpc.impl.TypeHandler;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This class generates a class with name 'typeSerializerClassName' that is able
 * to serialize and deserialize a set of types into or out of a stream. The set
 * of types is obtained from the SerializableTypeOracle object.
 */
public class TypeSerializerCreator {

  /**
   * Configuration property to use type indices instead of type signatures.
   */
  public static final String GWT_ELIDE_TYPE_NAMES_FROM_RPC = "gwt.elideTypeNamesFromRPC";

  /**
   * Default number of types to split createMethodMap entries into. Zero means
   * no sharding occurs. Stored as a string since it is used as a default
   * property value.
   *
   * Note that the inliner will likely reassemble the shards if it is used in
   * Production Mode, but it isn't needed there anyway.
   *
   * TODO: remove this (and related code) when it is no longer needed.
   */
  private static final String DEFAULT_CREATEMETHODMAP_SHARD_SIZE = "0";

  /**
   * Java system property name to override the above.
   */
  private static final String GWT_CREATEMETHODMAP_SHARD_SIZE = "gwt.typecreator.shard.size";

  private static int shardSize = -1;

  /**
   * The maximum number of bytes that should be used in the source code of a method body.
   * The absolute maximum limit is 64k (or it will fail to compile) however 8k is the limit
   * for the JIT compiler (HotSpot) and methods over this limit will run slower. We will leave a 1k
   * buffer and set the limit at 7k.
   */
  private static final int MAX_BYTES_PER_METHOD = 7168;
  private static final String LOAD_METHODS_JAVA_HELPER_PREFIX  = "populateLoadMethodsJavaMap";
  private static final String LOAD_SIGNATURES_JAVA_HELPER_PREFIX = "populateLoadSignaturesJavaMap";

  private static void computeShardSize(TreeLogger logger) throws UnableToCompleteException {
    String shardSizeProperty =
        System.getProperty(GWT_CREATEMETHODMAP_SHARD_SIZE, DEFAULT_CREATEMETHODMAP_SHARD_SIZE);
    try {
      shardSize = Integer.valueOf(shardSizeProperty);
      if (shardSize < 0) {
        logger.log(TreeLogger.ERROR, GWT_CREATEMETHODMAP_SHARD_SIZE + " must be non-negative: "
            + shardSizeProperty);
        throw new UnableToCompleteException();
      }
    } catch (NumberFormatException e) {
      logger.log(TreeLogger.ERROR, "Property " + GWT_CREATEMETHODMAP_SHARD_SIZE + " not a number: "
          + shardSizeProperty, e);
      throw new UnableToCompleteException();
    }
  }

  private final GeneratorContext context;

  private final SerializableTypeOracle deserializationOracle;

  private final boolean elideTypeNames;

  private final JType[] serializableTypes;

  private final SerializableTypeOracle serializationOracle;

  private final SourceWriter srcWriter;

  private final TypeOracle typeOracle;

  private final String typeSerializerClassName;

  private final String typeSerializerSimpleName;

  private final Map<JType, String> typeStrings = new IdentityHashMap<JType, String>();

  private final Set<JType> typesNotUsingCustomFieldSerializers;

  private final Set<JType> customFieldSerializersUsed;

  public TypeSerializerCreator(TreeLogger logger, SerializableTypeOracle serializationOracle,
      SerializableTypeOracle deserializationOracle, GeneratorContext context,
      String typeSerializerClassName, String typeSerializerSimpleName)
      throws UnableToCompleteException {
    this.context = context;
    this.typeSerializerClassName = typeSerializerClassName;
    this.typeSerializerSimpleName = typeSerializerSimpleName;
    this.serializationOracle = serializationOracle;
    this.deserializationOracle = deserializationOracle;

    this.typeOracle = context.getTypeOracle();

    Set<JType> typesSet = new HashSet<JType>();
    typesSet.addAll(Arrays.asList(serializationOracle.getSerializableTypes()));
    typesSet.addAll(Arrays.asList(deserializationOracle.getSerializableTypes()));
    serializableTypes = typesSet.toArray(new JType[0]);
    Arrays.sort(serializableTypes, SerializableTypeOracleBuilder.JTYPE_COMPARATOR);

    srcWriter = getSourceWriter(logger, context);
    if (shardSize < 0) {
      computeShardSize(logger);
    }
    if (logger.isLoggable(TreeLogger.TRACE)) {
      logger.log(TreeLogger.TRACE, "Using a shard size of " + shardSize
          + " for TypeSerializerCreator createMethodMap");
    }

    try {
      ConfigurationProperty prop =
          context.getPropertyOracle().getConfigurationProperty(GWT_ELIDE_TYPE_NAMES_FROM_RPC);
      elideTypeNames = Boolean.parseBoolean(prop.getValues().get(0));
    } catch (BadPropertyValueException e) {
      logger.log(TreeLogger.ERROR, "The configuration property " + GWT_ELIDE_TYPE_NAMES_FROM_RPC
          + " was not defined. Is RemoteService.gwt.xml inherited?");
      throw new UnableToCompleteException();
    }

    if (context.isGeneratorResultCachingEnabled()) {
      typesNotUsingCustomFieldSerializers = new HashSet<JType>();
      customFieldSerializersUsed = new HashSet<JType>();
    } else {
      typesNotUsingCustomFieldSerializers = null;
      customFieldSerializersUsed = null;
    }
  }

  public Set<JType> getCustomSerializersUsed() {
    return customFieldSerializersUsed;
  }

  public Set<JType> getTypesNotUsingCustomSerializers() {
    return typesNotUsingCustomFieldSerializers;
  }

  public Map<JType, String> getTypeStrings() {
    return Collections.unmodifiableMap(typeStrings);
  }

  public String realize(TreeLogger logger) {
    logger =
        logger.branch(TreeLogger.DEBUG, "Generating TypeSerializer for service interface '"
            + typeSerializerClassName + "'", null);

    createFieldSerializers(logger, context);

    int index = 0;
    for (JType type : getSerializableTypes()) {

      String typeString;
      if (elideTypeNames) {
        typeString = Integer.toString(++index, Character.MAX_RADIX);
      } else {
        typeString = getTypeString(type);
      }
      typeStrings.put(type, typeString);
    }

    if (srcWriter != null) {
      writeStaticFields();
      writeStaticInitializer();

      if (context.isProdMode()) {
        writeLoadMethodsNative();
        writeLoadSignaturesNative();
      } else {
        writeLoadMethodsJava();
        writeLoadSignaturesJava();
      }

      writeConstructor();
      srcWriter.commit(logger);
    }

    return typeSerializerClassName;
  }

  /*
   * Create a field serializer for a type if it does not have a custom
   * serializer.
   */
  private void createFieldSerializer(TreeLogger logger, GeneratorContext ctx, JType type) {
    Event event = SpeedTracerLogger.start(CompilerEventType.GENERATOR_RPC_FIELD_SERIALIZER);
    try {
      assert (type != null);
      assert (serializationOracle.isSerializable(type) || deserializationOracle
          .isSerializable(type));

      JParameterizedType parameterizedType = type.isParameterized();
      if (parameterizedType != null) {
        createFieldSerializer(logger, ctx, parameterizedType.getRawType());
        return;
      }

      /*
       * Only a JClassType can reach this point in the code. JPrimitives have
       * been removed because their serialization is built in, interfaces have
       * been removed because they are not an instantiable type and
       * parameterized types have been broken down into their raw types.
       */
      assert (type.isClass() != null || type.isArray() != null);

      // get custom field serializer, if available
      JClassType customFieldSerializer =
          SerializableTypeOracleBuilder.findCustomFieldSerializer(typeOracle, type);

      if (ctx.isGeneratorResultCachingEnabled()) {
        // update cached info for next iteration
        if (customFieldSerializer != null) {
          customFieldSerializersUsed.add(customFieldSerializer);
        } else {
          typesNotUsingCustomFieldSerializers.add(type);
        }

        // check the cache for a valid field serializer for the current type
        if (findReusableCachedFieldSerializerIfAvailable(logger, ctx, type,
            customFieldSerializer)) {
          // we can skip re-generation of the field serializer for the current
          // type
          return;
        }
      }

      // generate a new field serializer
      FieldSerializerCreator creator =
          new FieldSerializerCreator(logger, context, serializationOracle, deserializationOracle,
              (JClassType) type, customFieldSerializer);
      creator.realize(logger, ctx);
    } finally {
      event.end();
    }
  }

  /*
   * Create all of the necessary field serializers.
   */
  private void createFieldSerializers(TreeLogger logger, GeneratorContext ctx) {
    JType[] types = getSerializableTypes();
    int typeCount = types.length;
    for (int typeIndex = 0; typeIndex < typeCount; ++typeIndex) {
      JType type = types[typeIndex];
      assert (type != null);

      createFieldSerializer(logger, ctx, type);
    }
  }

  /*
   * check whether we can use a previously generated version of a
   * FieldSerializer. If so, mark it for reuse, and return true. Otherwise
   * return false.
   */
  private boolean findReusableCachedFieldSerializerIfAvailable(TreeLogger logger,
      GeneratorContext ctx, JType type, JType customFieldSerializer) {

    CachedGeneratorResult lastResult = ctx.getCachedGeneratorResult();
    if (lastResult == null || !ctx.isGeneratorResultCachingEnabled()) {
      return false;
    }

    String fieldSerializerName = SerializationUtils.getStandardSerializerName((JClassType) type);

    if (type instanceof JClassType) {
      // check that it is available for reuse
      if (!lastResult.isTypeCached(fieldSerializerName)) {
        if (logger.isLoggable(TreeLogger.TRACE)) {
          if (ctx.getTypeOracle().findType(fieldSerializerName) == null) {
            logger.log(TreeLogger.TRACE, "No cached field serializer available for "
                + type.getQualifiedSourceName());
          }
        }
        return false;
      }
    } else {
      return false;
    }

    @SuppressWarnings("unchecked")
    CachedRpcTypeInformation cachedTypeInfo =
        (CachedRpcTypeInformation) lastResult.getClientData(ProxyCreator.CACHED_TYPE_INFO_KEY);

    assert cachedTypeInfo != null;
    boolean foundMatch = false;
    if (cachedTypeInfo.checkLastModifiedTime(type)
        && ((customFieldSerializer != null && cachedTypeInfo
            .checkLastModifiedTime(customFieldSerializer)) || (customFieldSerializer == null && cachedTypeInfo
            .checkTypeNotUsingCustomSerializer(type)))) {
      // use cached version, if available
      foundMatch = ctx.tryReuseTypeFromCache(fieldSerializerName);
    }

    if (logger.isLoggable(TreeLogger.TRACE)) {
      String msg;
      if (foundMatch) {
        msg = "Reusing cached field serializer for " + type.getQualifiedSourceName();
      } else {
        msg = "Can't reuse cached field serializer for " + type.getQualifiedSourceName();
      }
      logger.log(TreeLogger.TRACE, msg);
    }

    return foundMatch;
  }

  private String[] getPackageAndClassName(String fullClassName) {
    String className = fullClassName;
    String packageName = "";
    int index = -1;
    if ((index = className.lastIndexOf('.')) >= 0) {
      packageName = className.substring(0, index);
      className = className.substring(index + 1, className.length());
    }
    return new String[] {packageName, className};
  }

  private JType[] getSerializableTypes() {
    return serializableTypes;
  }

  private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx) {
    String name[] = getPackageAndClassName(typeSerializerClassName);
    String packageName = name[0];
    String className = name[1];
    PrintWriter printWriter = ctx.tryCreate(logger, packageName, className);
    if (printWriter == null) {
      return null;
    }

    ClassSourceFileComposerFactory composerFactory =
        new ClassSourceFileComposerFactory(packageName, className);

    composerFactory.addImport(GWT.class.getName());
    composerFactory.addImport(JsArrayString.class.getName());
    composerFactory.addImport(TypeHandler.class.getName());
    composerFactory.addImport(HashMap.class.getName());
    composerFactory.addImport(Map.class.getName());
    composerFactory.addImport(GwtScriptOnly.class.getName());

    composerFactory.setSuperclass(SerializerBase.class.getName());
    return composerFactory.createSourceWriter(ctx, printWriter);
  }

  private String getTypeString(JType type) {
    String typeString =
        SerializationUtils.getRpcTypeName(type) + "/"
            + SerializationUtils.getSerializationSignature(context, type);
    return typeString;
  }

  /**
   * Return <code>true</code> if the custom field serializer has an instantiate
   * method.
   */
  private boolean hasInstantiateMethod(JClassType customSerializer, JType type) {
    return CustomFieldSerializerValidator.getInstantiationMethod(customSerializer,
        (JClassType) type) != null;
  }

  /**
   * Writes constructor.
   *
   * <pre>
   * public SchoolCalendarService_TypeSerializer() {
   *   super(methodMapJava, methodMapNative, signatureMapJava, signatureMapNative);
   * }
   * </pre>
   */
  private void writeConstructor() {
    srcWriter.println("public " + typeSerializerSimpleName + "() {");
    if (context.isProdMode()) {
      srcWriter.indentln("super(null, methodMapNative, null, signatureMapNative);");
    } else {
      srcWriter.indentln("super(methodMapJava, null, signatureMapJava, null);");
    }
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * Writes a method to produce a map of type string -> class name of
   * {@link TypeHandler} for Java. We are forced to use repeated helper methods to populate the map
   * because of the limit on Java method size.
   *
   * <pre>
   * private static Map&lt;String, String&gt; loadMethodsJava() {
   *   Map&lt;String, String&gt; result = new HashMap&lt;String, String&gt;();
   *   populateLoadMethodsJavaMap0(result);
   *   populateLoadMethodsJavaMap1(result);
   *   ...
   *   return result;
   * }
   * </pre>
   */
  private void writeLoadMethodsJava() {
    List<JType> filteredTypes = new ArrayList<JType>();
    JType[] types = getSerializableTypes();
    int n = types.length;
    for (int index = 0; index < n; ++index) {
      JType type = types[index];
      if (serializationOracle.maybeInstantiated(type)
          || deserializationOracle.maybeInstantiated(type)) {
        filteredTypes.add(type);
      }
    }

    // Write out as many helper methods as necessary.
    Iterator<JType> filteredTypesIter = filteredTypes.iterator();
    int numHelpers = 0;
    while (filteredTypesIter.hasNext()) {
      writeLoadMethodsJavaHelper(filteredTypesIter, Integer.toString(numHelpers));
      numHelpers++;
    }

    srcWriter.println("@SuppressWarnings(\"deprecation\")");
    srcWriter.println("private static Map<String, String> loadMethodsJava() {");
    srcWriter.indent();
    srcWriter.println("Map<String, String> result = new HashMap<String, String>();");

    // Call all the helper methods to populate the map.
    for (int helperNum = 0; helperNum < numHelpers; helperNum++) {
      srcWriter.println(LOAD_METHODS_JAVA_HELPER_PREFIX + Integer.toString(helperNum) +
          "(result);");
    }

    srcWriter.println("return result;");
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * Writes a helper method to add the type signatures for the specified types to the result map.
   * Adds as many of the types as possible by destructively modifying the iterator but may
   * finish with elements still left.
   *
   * <pre>
   * private static void populateLoadMethodsJavaMap#(Map&lt;String, String&gt; result) {
   *   result.put(
   *       &quot;java.lang.String/2004016611&quot;,
   *       &quot;com.google.gwt.user.client.rpc.core.java.lang.String_FieldSerializer&quot);
   *   ...
   * }
   * </pre>
   */
  private void writeLoadMethodsJavaHelper(Iterator<JType> iter, String methodSuffix) {
    srcWriter.println("private static void " + LOAD_METHODS_JAVA_HELPER_PREFIX + methodSuffix);
    srcWriter.println("(Map<String, String> result) {");
    srcWriter.indent();

    int totalBytesEstimate = 0;
    while (iter.hasNext() && totalBytesEstimate < MAX_BYTES_PER_METHOD) {
      JType type = iter.next();
      String typeString = typeStrings.get(type);
      assert typeString != null : "Missing type signature for " + type.getQualifiedSourceName();

      String line = "result.put(\"" + typeString + "\", \""
          + SerializationUtils.getStandardSerializerName((JClassType) type) + "\");";
      totalBytesEstimate += line.getBytes().length;
      srcWriter.println(line);
    }

    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * Writes a method to produce a native map of type string -> handler funcs.
   *
   * <pre>
   * private static native MethodMap loadMethodsNative() /&#42;-{
   *     var result = {};
   *     result["java.lang.String/2004016611"] = [
   *         @com.google.gwt.user.client.rpc.core.java.lang.String_CustomFieldSerializer::instantiate(Lcom/google/gwt/user/client/rpc/SerializationStreamReader;),
   *         @com.google.gwt.user.client.rpc.core.java.lang.String_CustomFieldSerializer::deserialize(Lcom/google/gwt/user/client/rpc/SerializationStreamReader;Ljava/lang/String;),
   *         @com.google.gwt.user.client.rpc.core.java.lang.String_CustomFieldSerializer::serialize(Lcom/google/gwt/user/client/rpc/SerializationStreamWriter;Ljava/lang/String;)
   *       ];
   *     ...
   *     return result;
   *   }-&#42;/;
   * </pre>
   */
  private void writeLoadMethodsNative() {
    srcWriter.println("@SuppressWarnings(\"deprecation\")");
    srcWriter.println("@GwtScriptOnly");
    srcWriter.println("private static native MethodMap loadMethodsNative() /*-{");
    srcWriter.indent();
    srcWriter.println("var result = {};");

    List<JType> filteredTypes = new ArrayList<JType>();
    JType[] types = getSerializableTypes();
    int n = types.length;
    for (int index = 0; index < n; ++index) {
      JType type = types[index];
      if (serializationOracle.maybeInstantiated(type)
          || deserializationOracle.maybeInstantiated(type)) {
        filteredTypes.add(type);
      }
    }

    boolean shard = shardSize > 0 && filteredTypes.size() > shardSize;
    int shardCount = 0;

    if (shard) {
      srcWriter.println("(function() {");
    }

    for (JType type : filteredTypes) {
      if (shard && ++shardCount % shardSize == 0) {
        srcWriter.println("})();");
        srcWriter.println("(function() {");
      }

      String typeString = typeStrings.get(type);
      assert typeString != null : "Missing type signature for " + type.getQualifiedSourceName();
      srcWriter.println("result[\"" + typeString + "\"] = [");

      srcWriter.indent();
      writeTypeMethodsNative(type);
      srcWriter.outdent();

      srcWriter.indentln("];");
      srcWriter.println();
    }

    if (shard) {
      srcWriter.println("})();");
    }

    srcWriter.println("return result;");
    srcWriter.outdent();
    srcWriter.println("}-*/;");
    srcWriter.println();
  }

  /**
   * Writes a method to produce a map of class name to type string for Java.
   *
   * <pre>
   * private static Map&lt;String&lt;?&gt;, String&gt; loadSignaturesJava() {
   *   Map&lt;String&lt;?&gt;, String&gt; result = new HashMap&lt;String&lt;?&gt;, String&gt;();
   *   populateLoadSignaturesJavaMap0(result);
   *   populateLoadSignaturesJavaMap1(result);
   *   ...
   *   return result;
   * }
   * </pre>
   */
  private void writeLoadSignaturesJava() {

    List<JType> allTypes = Arrays.asList(getSerializableTypes());
    Iterator<JType> allTypesIter = allTypes.iterator();

    // Write out as many helper methods as necessary.
    int numHelpers = 0;
    while (allTypesIter.hasNext()) {
      writeLoadSignaturesJavaHelper(allTypesIter, Integer.toString(numHelpers));
      numHelpers++;
    }

    srcWriter.println("@SuppressWarnings(\"deprecation\")");
    srcWriter.println("private static Map<String, String> loadSignaturesJava() {");
    srcWriter.indent();
    srcWriter.println("Map<String, String> result = new HashMap<String, String>();");

    // Call all the helper methods to populate the map.
    for (int helperNum = 0; helperNum < numHelpers; helperNum++) {
      srcWriter.println(LOAD_SIGNATURES_JAVA_HELPER_PREFIX +
          Integer.toString(helperNum) + "(result);");
    }

    srcWriter.println("return result;");
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * Writes a helper method to produce a map of class name to type string for Java. Adds as many of
   * the signatures as possible by destructively modifying the iterator but may finish with elements
   * still left.
   *
   * <pre>
   * private static void populateLoadSignaturesJavaMap#(Map&lt;String&lt;?&gt;, String&gt; result) {
   *   result.put(
   *       com.google.gwt.user.client.rpc.core.java.lang.String_FieldSerializer.concreteType(),
   *       &quot;java.lang.String/2004016611&quot;);
   *   ...
   * }
   * </pre>
   */
  private void writeLoadSignaturesJavaHelper(Iterator<JType> iter, String methodSuffix) {
    srcWriter.println("private static void " + LOAD_SIGNATURES_JAVA_HELPER_PREFIX +
        methodSuffix);
    srcWriter.println("(Map<String, String> result) {");
    srcWriter.indent();

    int totalBytesEstimate = 0;
    while (iter.hasNext() && totalBytesEstimate < MAX_BYTES_PER_METHOD) {
      JType type = iter.next();
      String typeString = typeStrings.get(type);

      if (!serializationOracle.maybeInstantiated(type)
          && !deserializationOracle.maybeInstantiated(type)) {
        continue;
      }

      String typeRef;
      JClassType customSerializer =
          SerializableTypeOracleBuilder.findCustomFieldSerializer(typeOracle, type);
      if (customSerializer != null
          && CustomFieldSerializerValidator.getConcreteTypeMethod(customSerializer) != null) {
        typeRef = customSerializer.getQualifiedSourceName() + ".concreteType()";
      } else {
        typeRef = '"' + SerializationUtils.getRpcTypeName(type) + '"';
      }

      String line = "result.put(" + typeRef + ", \"" + typeString + "\");";
      totalBytesEstimate += line.getBytes().length;
      srcWriter.println(line);
    }

    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * Writes a method to produce a native map of system hash code to type string.
   *
   * <pre>
   * private static native JsArrayString loadSignaturesNative() /*-{
   *   var result = [];
   *   result[@com.google.gwt.core.client.impl.Impl::getHashCode(Ljava/lang/Object;)(@java.lang.String::class)] = &quot;java.lang.String/2004016611&quot;;
   *   ...
   *   return result;
   * }-&#42;/;
   * </pre>
   */
  private void writeLoadSignaturesNative() {
    srcWriter.println("@SuppressWarnings(\"deprecation\")");
    srcWriter.println("@GwtScriptOnly");
    srcWriter.println("private static native JsArrayString loadSignaturesNative() /*-{");
    srcWriter.indent();
    srcWriter.println("var result = [];");

    boolean shard = shardSize > 0 && getSerializableTypes().length > shardSize;
    int shardCount = 0;

    if (shard) {
      srcWriter.println("(function() {");
    }

    for (JType type : getSerializableTypes()) {
      String typeString = typeStrings.get(type);

      if (!serializationOracle.maybeInstantiated(type)
          && !deserializationOracle.maybeInstantiated(type)) {
        continue;
      }

      if (shard && ++shardCount % shardSize == 0) {
        srcWriter.println("})();");
        srcWriter.println("(function() {");
      }

      srcWriter
          .println("result[@javaemul.internal.HashCodes::getObjectIdentityHashCode(*)(@"
              + type.getQualifiedSourceName() + "::class)] = \"" + typeString + "\";");
    }

    if (shard) {
      srcWriter.println("})();");
    }

    srcWriter.println("return result;");
    srcWriter.outdent();
    srcWriter.println("}-*/;");
    srcWriter.println();
  }

  /**
   * Writes the class's static fields.
   *
   * <pre>
   * private static final Map&lt;String, String&gt; methodMapJava;
   * private static final MethodMap methodMapNative;
   * private static final Map&lt;String&lt;?&gt;, String&gt; signatureMapJava;
   * private static final JsArrayString signatureMapNative;
   * </pre>
   */
  private void writeStaticFields() {
    if (context.isProdMode()) {
      srcWriter.println("private static final MethodMap methodMapNative;");
      srcWriter.println("private static final JsArrayString signatureMapNative;");
    } else {
      srcWriter.println("private static final Map<String, String> methodMapJava;");
      srcWriter.println("private static final Map<String, String> signatureMapJava;");
    }
    srcWriter.println();
  }

  /**
   * Statically initializes the class fields either for script or JVM.
   *
   * <pre>
   * static {
   *   if (GWT.isScript()) {
   *     methodMapJava = null;
   *     methodMapNative = loadMethodsNative();
   *     signatureMapJava = null;
   *     signatureMapNative = loadSignaturesNative();
   *   } else {
   *     methodMapJava = loadMethodsJava();
   *     methodMapNative = null;
   *     signatureMapJava = loadSignaturesJava();
   *     signatureMapNative = null;
   *   }
   * }
   * </pre>
   */
  private void writeStaticInitializer() {
    srcWriter.println("static {");
    srcWriter.indent();
    if (context.isProdMode()) {
      srcWriter.println("methodMapNative = loadMethodsNative();");
      srcWriter.println("signatureMapNative = loadSignaturesNative();");
    } else {
      srcWriter.println("methodMapJava = loadMethodsJava();");
      srcWriter.println("signatureMapJava = loadSignaturesJava();");
    }
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * Write an entry in the methodMapNative for one type.
   *
   * @param type type to generate entry for
   */
  private void writeTypeMethodsNative(JType type) {
    srcWriter.indent();
    String serializerName = SerializationUtils.getFieldSerializerName(typeOracle, type);
    JClassType customSerializer =
        SerializableTypeOracleBuilder.findCustomFieldSerializer(typeOracle, type);

    // First the initialization method
    if (deserializationOracle.maybeInstantiated(type)) {
      srcWriter.print("@");
      if (customSerializer != null) {
        if (hasInstantiateMethod(customSerializer, type)) {
          srcWriter.print(serializerName);
        } else {
          srcWriter.print(SerializationUtils.getStandardSerializerName((JClassType) type));
        }
      } else {
        srcWriter.print(serializerName);
      }
      srcWriter.print("::instantiate");
      srcWriter.print("(L" + SerializationStreamReader.class.getName().replace('.', '/') + ";)");
    }
    srcWriter.println(",");

    // Now the deserialization method
    if (deserializationOracle.isSerializable(type)) {
      // Assume param type is the concrete type of the serialized type.
      JType paramType = type;
      if (customSerializer != null) {
        // But a custom serializer may specify a looser type.
        JMethod deserializationMethod =
            CustomFieldSerializerValidator.getDeserializationMethod(customSerializer,
                (JClassType) type);
        paramType = deserializationMethod.getParameters()[1].getType();
      }
      srcWriter.print("@" + serializerName);
      srcWriter.print("::deserialize(L"
          + SerializationStreamReader.class.getName().replace('.', '/') + ";"
          + paramType.getJNISignature() + ")");
    }
    srcWriter.println(",");

    // Now the serialization method
    if (serializationOracle.isSerializable(type)) {
      // Assume param type is the concrete type of the serialized type.
      JType paramType = type;
      if (customSerializer != null) {
        // But a custom serializer may specify a looser type.
        JMethod serializationMethod =
            CustomFieldSerializerValidator.getSerializationMethod(customSerializer,
                (JClassType) type);
        paramType = serializationMethod.getParameters()[1].getType();
      }
      srcWriter.print("@" + serializerName);
      srcWriter.print("::serialize(L" + SerializationStreamWriter.class.getName().replace('.', '/')
          + ";" + paramType.getJNISignature() + ")");
      srcWriter.println();
    }
    srcWriter.outdent();
  }
}

