/*
 * 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.impl.Impl;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.CachedGeneratorResult;
import com.google.gwt.core.ext.CachedPropertyInformation;
import com.google.gwt.core.ext.ConfigurationProperty;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.RebindMode;
import com.google.gwt.core.ext.RebindResult;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.GeneratedResource;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.generator.NameFactory;
import com.google.gwt.dev.util.Util;
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.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
import com.google.gwt.user.client.rpc.RpcToken;
import com.google.gwt.user.client.rpc.RpcToken.RpcTokenImplementation;
import com.google.gwt.user.client.rpc.RpcTokenException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.client.rpc.impl.ClientSerializationStreamWriter;
import com.google.gwt.user.client.rpc.impl.FailedRequest;
import com.google.gwt.user.client.rpc.impl.FailingRequestBuilder;
import com.google.gwt.user.client.rpc.impl.RemoteServiceProxy;
import com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter.ResponseReader;
import com.google.gwt.user.client.rpc.impl.RpcStatsContext;
import com.google.gwt.user.linker.rpc.RpcLogArtifact;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.server.rpc.SerializationPolicyLoader;
import com.google.gwt.user.server.rpc.impl.TypeNameObfuscator;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Creates a client-side proxy for a
 * {@link com.google.gwt.user.client.rpc.RemoteService RemoteService} interface
 * as well as the necessary type and field serializers.
 */
public class ProxyCreator {
  /**
   * Some keys for storing cached information for use with generator result
   * caching.
   */
  public static final String CACHED_PROPERTY_INFO_KEY = "cached-property-info";
  public static final String CACHED_TYPE_INFO_KEY = "cached-type-info";

  /**
   * The directory within which RPC manifests are placed for individual
   * permutations.
   */
  public static final String MANIFEST_ARTIFACT_DIR = "rpcPolicyManifest/manifests";

  /**
   * Properties which need to be checked to determine cache reusability.
   */
  private static final Collection<String> configPropsToCheck = Arrays.asList(
      TypeSerializerCreator.GWT_ELIDE_TYPE_NAMES_FROM_RPC, Shared.RPC_ENHANCED_CLASSES);
  private static final Collection<String> selectionPropsToCheck = Arrays
      .asList(Shared.RPC_PROP_SUPPRESS_NON_STATIC_FINAL_FIELD_WARNINGS);

  private static final String PROXY_SUFFIX = "_Proxy";

  private static final Map<JPrimitiveType, ResponseReader> JPRIMITIVETYPE_TO_RESPONSEREADER =
      new HashMap<JPrimitiveType, ResponseReader>();
  static {
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.BOOLEAN, ResponseReader.BOOLEAN);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.BYTE, ResponseReader.BYTE);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.CHAR, ResponseReader.CHAR);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.DOUBLE, ResponseReader.DOUBLE);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.FLOAT, ResponseReader.FLOAT);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.INT, ResponseReader.INT);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.LONG, ResponseReader.LONG);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.SHORT, ResponseReader.SHORT);
    JPRIMITIVETYPE_TO_RESPONSEREADER.put(JPrimitiveType.VOID, ResponseReader.VOID);
  }

  /**
   * Adds a root type for each type that appears in the RemoteService interface
   * methods.
   */
  private static void addRemoteServiceRootTypes(TreeLogger logger, TypeOracle typeOracle,
      SerializableTypeOracleBuilder typesSentFromBrowser,
      SerializableTypeOracleBuilder typesSentToBrowser, JClassType remoteService)
      throws NotFoundException, UnableToCompleteException {
    logger =
        logger.branch(TreeLogger.DEBUG, "Analyzing '"
            + remoteService.getParameterizedQualifiedSourceName() + "' for serializable types",
            null);

    JMethod[] methods = remoteService.getOverridableMethods();

    JClassType exceptionClass = typeOracle.getType(Exception.class.getName());

    JClassType rteType = typeOracle.getType(RpcTokenException.class.getName());
    JClassType rpcTokenClass = typeOracle.getType(RpcToken.class.getName());
    RpcTokenImplementation tokenClassToUse =
        remoteService.findAnnotationInTypeHierarchy(RpcTokenImplementation.class);
    if (tokenClassToUse != null) {
      // only include serializer for the specified class literal
      JClassType rpcTokenType = typeOracle.getType(tokenClassToUse.value());
      if (rpcTokenType.isAssignableTo(rpcTokenClass)) {
        typesSentFromBrowser.addRootType(logger, rpcTokenType);
        typesSentToBrowser.addRootType(logger, rteType);
      } else {
        logger.branch(TreeLogger.ERROR, "RPC token class " + tokenClassToUse.value()
            + " must implement " + RpcToken.class.getName(), null);
        throw new UnableToCompleteException();
      }
    } else {
      JClassType[] rpcTokenSubclasses = rpcTokenClass.getSubtypes();
      for (JClassType rpcTokenSubclass : rpcTokenSubclasses) {
        typesSentFromBrowser.addRootType(logger, rpcTokenSubclass);
      }
      if (rpcTokenSubclasses.length > 0) {
        typesSentToBrowser.addRootType(logger, rteType);
      }
    }

    TreeLogger validationLogger = logger.branch(TreeLogger.DEBUG, "Analyzing methods:", null);
    for (JMethod method : methods) {
      TreeLogger methodLogger = validationLogger.branch(TreeLogger.DEBUG, method.toString(), null);
      JType returnType = method.getReturnType();
      if (returnType != JPrimitiveType.VOID) {
        TreeLogger returnTypeLogger =
            methodLogger.branch(TreeLogger.DEBUG, "Return type: "
                + returnType.getParameterizedQualifiedSourceName(), null);
        typesSentToBrowser.addRootType(returnTypeLogger, returnType);
      }

      JParameter[] params = method.getParameters();
      for (JParameter param : params) {
        TreeLogger paramLogger =
            methodLogger.branch(TreeLogger.DEBUG, "Parameter: " + param.toString(), null);
        JType paramType = param.getType();
        typesSentFromBrowser.addRootType(paramLogger, paramType);
      }

      JType[] exs = method.getThrows();
      if (exs.length > 0) {
        TreeLogger throwsLogger = methodLogger.branch(TreeLogger.DEBUG, "Throws:", null);
        for (JType ex : exs) {
          if (!exceptionClass.isAssignableFrom(ex.isClass())) {
            throwsLogger =
                throwsLogger.branch(TreeLogger.WARN, "'" + ex.getQualifiedSourceName()
                    + "' is not a checked exception; only checked exceptions may be used", null);
          }

          typesSentToBrowser.addRootType(throwsLogger, ex);
        }
      }
    }
  }

  /**
   * Add the implicit root types that are needed to make RPC work. These would
   * be {@link String} and {@link IncompatibleRemoteServiceException}.
   */
  private static void addRequiredRoots(TreeLogger logger, TypeOracle typeOracle,
      SerializableTypeOracleBuilder stob) throws NotFoundException {
    logger = logger.branch(TreeLogger.DEBUG, "Analyzing implicit types");

    // String is always instantiable.
    JClassType stringType = typeOracle.getType(String.class.getName());
    stob.addRootType(logger, stringType);

    // IncompatibleRemoteServiceException is always serializable
    JClassType icseType = typeOracle.getType(IncompatibleRemoteServiceException.class.getName());
    stob.addRootType(logger, icseType);
  }

  /**
   * Take the union of two type arrays, and then sort the results
   * alphabetically.
   */
  private static JType[] unionOfTypeArrays(JType[]... types) {
    Set<JType> typesList = new HashSet<JType>();
    for (JType[] a : types) {
      typesList.addAll(Arrays.asList(a));
    }
    JType[] serializableTypes = typesList.toArray(new JType[typesList.size()]);
    Arrays.sort(serializableTypes, SerializableTypeOracleBuilder.JTYPE_COMPARATOR);
    return serializableTypes;
  }

  protected JClassType serviceIntf;

  private boolean elideTypeNames;

  /**
   * The possibly obfuscated type signatures used to represent a type.
   */
  private Map<JType, String> typeStrings;

  private Set<JType> customSerializersUsed;
  private Set<JType> typesNotUsingCustomSerializers;

  public ProxyCreator(JClassType serviceIntf) {
    assert (serviceIntf.isInterface() != null);
    this.serviceIntf = serviceIntf;
  }

  /**
   * Creates the client-side proxy class.
   *
   * @throws UnableToCompleteException
   */
  public RebindResult create(TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    TypeOracle typeOracle = context.getTypeOracle();

    JClassType serviceAsync = typeOracle.findType(serviceIntf.getQualifiedSourceName() + "Async");
    if (serviceAsync == null) {
      logger.branch(TreeLogger.ERROR,
          "Could not find an asynchronous version for the service interface "
              + serviceIntf.getQualifiedSourceName(), null);
      RemoteServiceAsyncValidator.logValidAsyncInterfaceDeclaration(logger, serviceIntf);
      throw new UnableToCompleteException();
    }

    if (checkAlreadyGenerated(typeOracle, serviceIntf)) {
      return new RebindResult(RebindMode.USE_EXISTING, getProxyQualifiedName());
    }

    // Make sure that the async and synchronous versions of the RemoteService
    // agree with one another
    //
    RemoteServiceAsyncValidator rsav = new RemoteServiceAsyncValidator(logger, typeOracle);
    Map<JMethod, JMethod> syncMethToAsyncMethMap = rsav.validate(logger, serviceIntf, serviceAsync);

    final PropertyOracle propertyOracle = context.getPropertyOracle();

    // Load the blacklist/whitelist
    TypeFilter blacklistTypeFilter = new BlacklistTypeFilter(logger, propertyOracle);

    // Determine the set of serializable types
    Event event = SpeedTracerLogger.start(CompilerEventType.GENERATOR_RPC_STOB);
    SerializableTypeOracle typesSentFromBrowser;
    SerializableTypeOracle typesSentToBrowser;
    String rpcLog = null;
    try {
      SerializableTypeOracleBuilder typesSentFromBrowserBuilder =
          new SerializableTypeOracleBuilder(logger, context);
      typesSentFromBrowserBuilder.setTypeFilter(blacklistTypeFilter);
      SerializableTypeOracleBuilder typesSentToBrowserBuilder =
          new SerializableTypeOracleBuilder(logger, context);
      typesSentToBrowserBuilder.setTypeFilter(blacklistTypeFilter);

      addRoots(logger, typeOracle, typesSentFromBrowserBuilder, typesSentToBrowserBuilder);

      // Decide what types to send in each direction.
      {
        if (logger.isLoggable(TreeLogger.Type.DEBUG)) {
          // Log the decisions to a string that will be written later in this method
          StringWriter stringWriter = new StringWriter();
          PrintWriter writer = new PrintWriter(stringWriter);

          typesSentFromBrowserBuilder.setLogOutputWriter(writer);
          typesSentToBrowserBuilder.setLogOutputWriter(writer);

          writer.write("====================================\n");
          writer.write("Types potentially sent from browser:\n");
          writer.write("====================================\n\n");
          writer.flush();
          typesSentFromBrowser = typesSentFromBrowserBuilder.build(logger);

          writer.write("===================================\n");
          writer.write("Types potentially sent from server:\n");
          writer.write("===================================\n\n");
          writer.flush();
          typesSentToBrowser = typesSentToBrowserBuilder.build(logger);

          writer.close();
          rpcLog = stringWriter.toString();
        } else {
          typesSentFromBrowser = typesSentFromBrowserBuilder.build(logger);
          typesSentToBrowser = typesSentToBrowserBuilder.build(logger);
        }
      }

    } finally {
      event.end();
    }

    // Check previous cached result, to see if we can return now
    if (checkCachedGeneratorResultValid(logger, context, typesSentFromBrowser, typesSentToBrowser)) {
      logger.log(TreeLogger.TRACE, "Reusing all cached artifacts for " + getProxyQualifiedName());
      return new RebindResult(RebindMode.USE_ALL_CACHED, getProxyQualifiedName());
    }

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

    SourceWriter srcWriter = getSourceWriter(logger, context, serviceAsync);
    if (srcWriter == null) {
      // don't expect this to occur, but could happen if an instance was
      // recently generated but not yet committed
      return new RebindResult(RebindMode.USE_EXISTING, getProxyQualifiedName());
    }

    generateTypeHandlers(logger, context, typesSentFromBrowser, typesSentToBrowser);

    String serializationPolicyStrongName =
        writeSerializationPolicyFile(logger, context, typesSentFromBrowser, typesSentToBrowser);

    String remoteServiceInterfaceName =
        elideTypeNames ? TypeNameObfuscator.SERVICE_INTERFACE_ID : SerializationUtils
            .getRpcTypeName(serviceIntf);
    generateProxyFields(srcWriter, typesSentFromBrowser, serializationPolicyStrongName,
        remoteServiceInterfaceName);

    generateProxyContructor(srcWriter);

    generateProxyMethods(srcWriter, typesSentFromBrowser, typeOracle, syncMethToAsyncMethMap);

    generateStreamWriterOverride(srcWriter);

    generateCheckRpcTokenTypeOverride(srcWriter, typeOracle, typesSentFromBrowser);

    srcWriter.commit(logger);

    if (rpcLog != null) {
      // Create an artifact explaining STOB's decisions. It will be emitted by
      // RpcLogLinker
      context.commitArtifact(logger, new RpcLogArtifact(serviceIntf.getQualifiedSourceName(),
          serializationPolicyStrongName, rpcLog));
    }

    if (checkGeneratorResultCacheability(context)) {
      /*
       * Create a new cacheable result. The mode is set to
       * RebindMode.USE_PARTIAL_CACHED, since we are allowing reuse of cached
       * results for field serializers, when available, but all other types have
       * been newly generated.
       */
      RebindResult result =
          new RebindResult(RebindMode.USE_PARTIAL_CACHED, getProxyQualifiedName());

      // Remember the type info that we care about for cache reuse testing.
      CachedRpcTypeInformation cti =
          new CachedRpcTypeInformation(typesSentFromBrowser, typesSentToBrowser,
              customSerializersUsed, typesNotUsingCustomSerializers);
      CachedPropertyInformation cpi =
          new CachedPropertyInformation(logger, context.getPropertyOracle(), selectionPropsToCheck,
              configPropsToCheck);
      result.putClientData(CACHED_TYPE_INFO_KEY, cti);
      result.putClientData(CACHED_PROPERTY_INFO_KEY, cpi);

      return result;
    } else {
      return new RebindResult(RebindMode.USE_ALL_NEW_WITH_NO_CACHING, getProxyQualifiedName());
    }
  }

  protected void addRoots(TreeLogger logger, TypeOracle typeOracle,
      SerializableTypeOracleBuilder typesSentFromBrowserBuilder,
      SerializableTypeOracleBuilder typesSentToBrowserBuilder) throws UnableToCompleteException {
    try {
      addRequiredRoots(logger, typeOracle, typesSentFromBrowserBuilder);
      addRequiredRoots(logger, typeOracle, typesSentToBrowserBuilder);

      addRemoteServiceRootTypes(logger, typeOracle, typesSentFromBrowserBuilder,
          typesSentToBrowserBuilder, serviceIntf);
    } catch (NotFoundException e) {
      logger.log(TreeLogger.ERROR, "Unable to find type referenced from remote service", e);
      throw new UnableToCompleteException();
    }
  }

  protected String computeTypeNameExpression(JType paramType) {
    String typeName;
    if (typeStrings.containsKey(paramType)) {
      typeName = typeStrings.get(paramType);
    } else {
      typeName = SerializationUtils.getRpcTypeName(paramType);
    }
    return typeName == null ? null : ('"' + typeName + '"');
  }

  protected void generateCheckRpcTokenTypeOverride(SourceWriter srcWriter, TypeOracle typeOracle,
      SerializableTypeOracle typesSentFromBrowser) {
    JClassType rpcTokenType = typeOracle.findType(RpcToken.class.getName());
    JClassType[] rpcTokenSubtypes = rpcTokenType.getSubtypes();
    String rpcTokenImplementation = "";
    for (JClassType rpcTokenSubtype : rpcTokenSubtypes) {
      if (typesSentFromBrowser.isSerializable(rpcTokenSubtype)) {
        if (rpcTokenImplementation.length() > 0) {
          // >1 implematation of RpcToken, bail
          rpcTokenImplementation = "";
          break;
        } else {
          rpcTokenImplementation = rpcTokenSubtype.getQualifiedSourceName();
        }
      }
    }
    if (rpcTokenImplementation.length() > 0) {
      srcWriter.println("@Override");
      srcWriter.println("protected void checkRpcTokenType(RpcToken token) {");
      srcWriter.indent();
      srcWriter.println("if (!(token instanceof " + rpcTokenImplementation + ")) {");
      srcWriter.indent();
      srcWriter.println("throw new RpcTokenException(\"Invalid RpcToken type: " + "expected '"
          + rpcTokenImplementation + "' but got '\" + " + "token.getClass() + \"'\");");
      srcWriter.outdent();
      srcWriter.println("}");
      srcWriter.outdent();
      srcWriter.println("}");
    }
  }

  /**
   * Generate the proxy constructor and delegate to the superclass constructor
   * using the default address for the
   * {@link com.google.gwt.user.client.rpc.RemoteService RemoteService}.
   */
  protected void generateProxyContructor(SourceWriter srcWriter) {
    srcWriter.println("public " + getProxySimpleName() + "() {");
    srcWriter.indent();
    srcWriter.println("super(GWT.getModuleBaseURL(),");
    srcWriter.indent();
    srcWriter.println(getRemoteServiceRelativePath() + ", ");
    srcWriter.println("SERIALIZATION_POLICY, ");
    srcWriter.println("SERIALIZER);");
    srcWriter.outdent();
    srcWriter.outdent();
    srcWriter.println("}");
  }

  /**
   * Generate any fields required by the proxy.
   *
   * @param serializableTypeOracle the type oracle
   */
  protected void generateProxyFields(SourceWriter srcWriter,
      SerializableTypeOracle serializableTypeOracle, String serializationPolicyStrongName,
      String remoteServiceInterfaceName) {
    // Initialize a field with binary name of the remote service interface
    srcWriter.println("private static final String REMOTE_SERVICE_INTERFACE_NAME = " + "\""
        + remoteServiceInterfaceName + "\";");
    srcWriter.println("private static final String SERIALIZATION_POLICY =\""
        + serializationPolicyStrongName + "\";");
    String typeSerializerName = SerializationUtils.getTypeSerializerQualifiedName(serviceIntf);
    srcWriter.println("private static final " + typeSerializerName + " SERIALIZER = new "
        + typeSerializerName + "();");
    srcWriter.println();
  }

  /**
   * Generates the client's asynchronous proxy method.
   *
   * @param serializableTypeOracle the type oracle
   */
  protected void generateProxyMethod(SourceWriter w, SerializableTypeOracle serializableTypeOracle,
      TypeOracle typeOracle, JMethod syncMethod, JMethod asyncMethod) {

    w.println();

    // Write the method signature
    JType asyncReturnType = asyncMethod.getReturnType().getErasedType();
    w.print("public ");
    w.print(asyncReturnType.getQualifiedSourceName());
    w.print(" ");
    w.print(asyncMethod.getName() + "(");

    boolean needsComma = false;
    NameFactory nameFactory = new NameFactory();
    JParameter[] asyncParams = asyncMethod.getParameters();
    for (int i = 0; i < asyncParams.length; ++i) {
      JParameter param = asyncParams[i];

      if (needsComma) {
        w.print(", ");
      } else {
        needsComma = true;
      }

      /*
       * Ignoring the AsyncCallback parameter, if any method requires a call to
       * SerializationStreamWriter.writeObject we need a try catch block
       */
      JType paramType = param.getType();
      paramType = paramType.getErasedType();

      w.print(paramType.getQualifiedSourceName());
      w.print(" ");

      String paramName = param.getName();
      nameFactory.addName(paramName);
      w.print(paramName);
    }

    w.println(") {");
    w.indent();

    String helperName = nameFactory.createName("helper");
    String helperClassName = RemoteServiceProxy.ServiceHelper.class.getCanonicalName();
    w.println("%s %s = new %s(\"%s\", \"%s\");", helperClassName, helperName, helperClassName,
        getProxySimpleName(), syncMethod.getName());

    w.println("try {");
    w.indent();

    // Write the parameter count followed by the parameter values
    JParameter[] syncParams = syncMethod.getParameters();

    String streamWriterName = nameFactory.createName("streamWriter");
    w.println("%s %s = %s.start(REMOTE_SERVICE_INTERFACE_NAME, %s);",
        SerializationStreamWriter.class.getSimpleName(), streamWriterName, helperName,
        syncParams.length);

    for (JParameter param : syncParams) {
      JType paramType = param.getType().getErasedType();
      String typeNameExpression = computeTypeNameExpression(paramType);
      assert typeNameExpression != null : "Could not compute a type name for "
          + paramType.getQualifiedSourceName();
      w.println(streamWriterName + ".writeString(" + typeNameExpression + ");");
    }

    // Encode all of the arguments to the asynchronous method, but exclude the
    // last argument which is the callback instance.
    //
    for (int i = 0; i < asyncParams.length - 1; ++i) {
      JParameter asyncParam = asyncParams[i];
      w.print(streamWriterName + ".");
      w.print(Shared.getStreamWriteMethodNameFor(asyncParam.getType()));
      w.println("(" + asyncParam.getName() + ");");
    }

    /*
     * Depending on the return type for the async method, return a
     * RequestBuilder, a Request, or nothing at all.
     */
    JParameter callbackParam = asyncParams[asyncParams.length - 1];
    JType returnType = syncMethod.getReturnType();
    String callbackName = callbackParam.getName();

    if (asyncReturnType == JPrimitiveType.VOID) {
      w.println("%s.finish(%s, ResponseReader.%s);", helperName, callbackName,
          getResponseReaderFor(returnType).name());
    } else if (asyncReturnType.getQualifiedSourceName().equals(RequestBuilder.class.getName())) {
      w.println("return %s.finishForRequestBuilder(%s, ResponseReader.%s);", helperName,
          callbackName, getResponseReaderFor(returnType).name());
    } else if (asyncReturnType.getQualifiedSourceName().equals(Request.class.getName())) {
      w.println("return %s.finish(%s, ResponseReader.%s);", helperName, callbackName,
          getResponseReaderFor(returnType).name());
    } else {
      // This method should have been caught by RemoteServiceAsyncValidator
      throw new RuntimeException("Unhandled return type "
          + asyncReturnType.getQualifiedSourceName());
    }

    w.outdent();
    w.print("} catch (SerializationException ");
    String exceptionName = nameFactory.createName("ex");
    w.println(exceptionName + ") {");
    w.indent();
    if (!asyncReturnType.getQualifiedSourceName().equals(RequestBuilder.class.getName())) {
      /*
       * If the method returns void or Request, signal the serialization error
       * immediately. If the method returns RequestBuilder, the error will be
       * signaled whenever RequestBuilder.send() is invoked.
       */
      w.println(callbackName + ".onFailure(" + exceptionName + ");");
    }
    if (asyncReturnType.getQualifiedSourceName().equals(RequestBuilder.class.getName())) {
      w.println("return new " + FailingRequestBuilder.class.getName() + "(" + exceptionName + ", "
          + callbackName + ");");
    } else if (asyncReturnType.getQualifiedSourceName().equals(Request.class.getName())) {
      w.println("return new " + FailedRequest.class.getName() + "();");
    } else {
      assert asyncReturnType == JPrimitiveType.VOID;
    }
    w.outdent();
    w.println("}");

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

  protected void generateProxyMethods(SourceWriter w,
      SerializableTypeOracle serializableTypeOracle, TypeOracle typeOracle,
      Map<JMethod, JMethod> syncMethToAsyncMethMap) {
    JMethod[] syncMethods = serviceIntf.getOverridableMethods();
    for (JMethod syncMethod : syncMethods) {

      JMethod asyncMethod = syncMethToAsyncMethMap.get(syncMethod);
      assert (asyncMethod != null);

      JClassType enclosingType = syncMethod.getEnclosingType();
      JParameterizedType isParameterizedType = enclosingType.isParameterized();
      if (isParameterizedType != null) {
        JMethod[] methods = isParameterizedType.getMethods();
        for (int i = 0; i < methods.length; ++i) {
          if (methods[i] == syncMethod) {
            /*
             * Use the generic version of the method to ensure that the server
             * can find the method using the erasure of the generic signature.
             */
            syncMethod = isParameterizedType.getBaseType().getMethods()[i];
          }
        }
      }

      generateProxyMethod(w, serializableTypeOracle, typeOracle, syncMethod, asyncMethod);
    }
  }

  /**
   * @param syncMethod
   * @param asyncMethod
   * @param statsContextName
   */
  protected void generateRpcStatsContext(SourceWriter w, JMethod syncMethod, JMethod asyncMethod,
      String statsContextName) {
    w.println("RpcStatsContext " + statsContextName + " = new RpcStatsContext();");
  }

  protected void generateStreamWriterOverride(SourceWriter srcWriter) {
    srcWriter.println("@Override");
    srcWriter.println("public SerializationStreamWriter createStreamWriter() {");
    srcWriter.indent();
    /*
     * Need an explicit cast since we've widened the declaration of the method
     * in RemoteServiceProxy.
     */
    srcWriter.println("ClientSerializationStreamWriter toReturn =");
    srcWriter.indentln("(ClientSerializationStreamWriter) super.createStreamWriter();");
    if (elideTypeNames) {
      srcWriter.println("toReturn.addFlags(ClientSerializationStreamWriter."
          + "FLAG_ELIDE_TYPE_NAMES);");
    }
    srcWriter.println("if (getRpcToken() != null) {");
    srcWriter.indent();
    srcWriter.println("toReturn.addFlags(ClientSerializationStreamWriter."
        + "FLAG_RPC_TOKEN_INCLUDED);");
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println("return toReturn;");
    srcWriter.outdent();
    srcWriter.println("}");
  }

  protected void generateTypeHandlers(TreeLogger logger, GeneratorContext context,
      SerializableTypeOracle typesSentFromBrowser, SerializableTypeOracle typesSentToBrowser)
      throws UnableToCompleteException {
    Event event = SpeedTracerLogger.start(CompilerEventType.GENERATOR_RPC_TYPE_SERIALIZER);
    TypeSerializerCreator tsc =
        new TypeSerializerCreator(logger, typesSentFromBrowser, typesSentToBrowser, context,
            SerializationUtils.getTypeSerializerQualifiedName(serviceIntf), SerializationUtils
                .getTypeSerializerSimpleName(serviceIntf));
    tsc.realize(logger);
    event.end();

    typeStrings = new HashMap<JType, String>(tsc.getTypeStrings());
    typeStrings.put(serviceIntf, TypeNameObfuscator.SERVICE_INTERFACE_ID);

    customSerializersUsed = tsc.getCustomSerializersUsed();
    typesNotUsingCustomSerializers = tsc.getTypesNotUsingCustomSerializers();
  }

  protected String getProxySimpleName() {
    String[] name = Shared.synthesizeTopLevelClassName(serviceIntf, PROXY_SUFFIX);
    return name[1];
  }

  protected Class<? extends RemoteServiceProxy> getProxySupertype() {
    return RemoteServiceProxy.class;
  }

  protected String getRemoteServiceRelativePath() {
    RemoteServiceRelativePath moduleRelativeURL =
        serviceIntf.getAnnotation(RemoteServiceRelativePath.class);
    if (moduleRelativeURL != null) {
      return "\"" + moduleRelativeURL.value() + "\"";
    }

    return null;
  }

  protected Class<? extends SerializationStreamWriter> getStreamWriterClass() {
    return ClientSerializationStreamWriter.class;
  }

  protected String writeSerializationPolicyFile(TreeLogger logger, GeneratorContext ctx,
      SerializableTypeOracle serializationSto, SerializableTypeOracle deserializationSto)
      throws UnableToCompleteException {
    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      OutputStreamWriter osw =
          new OutputStreamWriter(baos, SerializationPolicyLoader.SERIALIZATION_POLICY_FILE_ENCODING);
      PrintWriter pw = new PrintWriter(osw);

      JType[] serializableTypes =
          unionOfTypeArrays(serializationSto.getSerializableTypes(), deserializationSto
              .getSerializableTypes(), new JType[] {serviceIntf});

      pw.print(SerializationPolicyLoader.FINAL_FIELDS_KEYWORD);
      pw.print(", ");
      pw.print(Shared.shouldSerializeFinalFields(logger, ctx));
      pw.print('\n');

      for (int i = 0; i < serializableTypes.length; ++i) {
        JType type = serializableTypes[i];
        String binaryTypeName = SerializationUtils.getRpcTypeName(type);
        pw.print(binaryTypeName);
        pw.print(", " + Boolean.toString(deserializationSto.isSerializable(type)));
        pw.print(", " + Boolean.toString(deserializationSto.maybeInstantiated(type)));
        pw.print(", " + Boolean.toString(serializationSto.isSerializable(type)));
        pw.print(", " + Boolean.toString(serializationSto.maybeInstantiated(type)));
        pw.print(", " + typeStrings.get(type));

        /*
         * Include the serialization signature to bump the RPC file name if
         * obfuscated identifiers are used.
         */
        pw.print(", " + SerializationUtils.getSerializationSignature(ctx, type));
        pw.print('\n');

        /*
         * Emit client-side field information for classes that may be enhanced
         * on the server. Each line consists of a comma-separated list
         * containing the keyword '@ClientFields', the class name, and a list of
         * all potentially serializable client-visible fields.
         */
        if ((type instanceof JClassType) && ((JClassType) type).isEnhanced()) {
          JField[] fields = ((JClassType) type).getFields();
          JField[] rpcFields = new JField[fields.length];
          int numRpcFields = 0;
          for (JField f : fields) {
            if (f.isTransient() || f.isStatic() || f.isFinal()) {
              continue;
            }
            rpcFields[numRpcFields++] = f;
          }

          pw.print(SerializationPolicyLoader.CLIENT_FIELDS_KEYWORD);
          pw.print(',');
          pw.print(binaryTypeName);
          for (int idx = 0; idx < numRpcFields; idx++) {
            pw.print(',');
            pw.print(rpcFields[idx].getName());
          }
          pw.print('\n');
        }
      }

      // Closes the wrapped streams.
      pw.close();

      byte[] serializationPolicyFileContents = baos.toByteArray();
      String serializationPolicyName = Util.computeStrongName(serializationPolicyFileContents);

      String serializationPolicyFileName =
          SerializationPolicyLoader.getSerializationPolicyFileName(serializationPolicyName);
      OutputStream os = ctx.tryCreateResource(logger, serializationPolicyFileName);
      if (os != null) {
        os.write(serializationPolicyFileContents);
        GeneratedResource resource = ctx.commitResource(logger, os);

        /*
         * Record which proxy class created the resource. A manifest will be
         * emitted by the RpcPolicyManifestLinker.
         */
        emitPolicyFileArtifact(logger, ctx, resource.getPartialPath());
      } else {
        if (logger.isLoggable(TreeLogger.TRACE)) {
          logger.log(TreeLogger.TRACE, "SerializationPolicy file for RemoteService '"
              + serviceIntf.getQualifiedSourceName() + "' already exists; no need to rewrite it.",
              null);
        }
      }

      return serializationPolicyName;
    } catch (UnsupportedEncodingException e) {
      logger.log(TreeLogger.ERROR, SerializationPolicyLoader.SERIALIZATION_POLICY_FILE_ENCODING
          + " is not supported", e);
      throw new UnableToCompleteException();
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, null, e);
      throw new UnableToCompleteException();
    }
  }

  private boolean checkAlreadyGenerated(TypeOracle typeOracle, JClassType serviceAsync) {
    JPackage serviceIntfPkg = serviceAsync.getPackage();
    String packageName = serviceIntfPkg == null ? "" : serviceIntfPkg.getName();
    return typeOracle.findType(packageName, getProxySimpleName()) != null;
  }

  private boolean checkCachedGeneratorResultValid(TreeLogger logger, GeneratorContext ctx,
      SerializableTypeOracle typesSentFromBrowser, SerializableTypeOracle typesSentToBrowser) {

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

    CachedPropertyInformation cpi =
        (CachedPropertyInformation) lastResult.getClientData(CACHED_PROPERTY_INFO_KEY);
    if (cpi == null) {
      return false;
    }

    CachedRpcTypeInformation cti =
        (CachedRpcTypeInformation) lastResult.getClientData(CACHED_TYPE_INFO_KEY);
    if (cti == null) {
      return false;
    }

    if (!cti.checkTypeInformation(logger, ctx.getTypeOracle(), typesSentFromBrowser,
        typesSentToBrowser)) {
      return false;
    }

    if (!cpi.checkPropertiesWithPropertyOracle(logger, ctx.getPropertyOracle())) {
      return false;
    }

    return true;
  }

  private boolean checkGeneratorResultCacheability(GeneratorContext context) {
    /*
     * Currently not supporting caching for implementations which sub-class this
     * class, such as {@link RpcProxyCreator}, which implements deRPC.
     */
    if (!this.getClass().equals(ProxyCreator.class)) {
      return false;
    }

    return context.isGeneratorResultCachingEnabled();
  }

  private void emitPolicyFileArtifact(TreeLogger logger, GeneratorContext context,
      String partialPath) throws UnableToCompleteException {
    try {
      String qualifiedSourceName = serviceIntf.getQualifiedSourceName();
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      Writer writer;
      writer =
          new OutputStreamWriter(baos, SerializationPolicyLoader.SERIALIZATION_POLICY_FILE_ENCODING);
      writer.write("serviceClass: " + qualifiedSourceName + "\n");
      writer.write("path: " + partialPath + "\n");
      writer.close();

      byte[] manifestBytes = baos.toByteArray();
      String md5 = Util.computeStrongName(manifestBytes);
      OutputStream os =
          context.tryCreateResource(logger, MANIFEST_ARTIFACT_DIR + "/" + md5 + ".txt");
      os.write(manifestBytes);

      GeneratedResource resource = context.commitResource(logger, os);
      // TODO: change to Deploy when possible
      resource.setVisibility(Visibility.LegacyDeploy);
    } catch (UnsupportedEncodingException e) {
      logger.log(TreeLogger.ERROR, SerializationPolicyLoader.SERIALIZATION_POLICY_FILE_ENCODING
          + " is not supported", e);
      throw new UnableToCompleteException();
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, null, e);
      throw new UnableToCompleteException();
    }
  }

  private String getProxyQualifiedName() {
    String[] name = Shared.synthesizeTopLevelClassName(serviceIntf, PROXY_SUFFIX);
    return name[0].length() == 0 ? name[1] : name[0] + "." + name[1];
  }

  private ResponseReader getResponseReaderFor(JType returnType) {
    if (returnType.isPrimitive() != null) {
      return JPRIMITIVETYPE_TO_RESPONSEREADER.get(returnType.isPrimitive());
    }

    if (returnType.getQualifiedSourceName().equals(String.class.getCanonicalName())) {
      return ResponseReader.STRING;
    }

    return ResponseReader.OBJECT;
  }

  private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx,
      JClassType serviceAsync) {
    JPackage serviceIntfPkg = serviceAsync.getPackage();
    String packageName = serviceIntfPkg == null ? "" : serviceIntfPkg.getName();
    PrintWriter printWriter = ctx.tryCreate(logger, packageName, getProxySimpleName());
    if (printWriter == null) {
      return null;
    }

    ClassSourceFileComposerFactory composerFactory =
        new ClassSourceFileComposerFactory(packageName, getProxySimpleName());

    String[] imports =
        new String[] {
            getProxySupertype().getCanonicalName(), getStreamWriterClass().getCanonicalName(),
            SerializationStreamWriter.class.getCanonicalName(), GWT.class.getCanonicalName(),
            ResponseReader.class.getCanonicalName(),
            SerializationException.class.getCanonicalName(), RpcToken.class.getCanonicalName(),
            RpcTokenException.class.getCanonicalName(), Impl.class.getCanonicalName(),
            RpcStatsContext.class.getCanonicalName()};
    for (String imp : imports) {
      composerFactory.addImport(imp);
    }

    composerFactory.setSuperclass(getProxySupertype().getSimpleName());
    composerFactory.addImplementedInterface(serviceAsync.getErasedType().getQualifiedSourceName());

    return composerFactory.createSourceWriter(ctx, printWriter);
  }
}
