// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.rebind;

import com.google.gwt.core.ext.Generator;
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.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.RpcRequestBuilder;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.rebind.rpc.ServiceInterfaceProxyGenerator;

import java.io.PrintWriter;

/**
 * Service proxy generator that can be used instead of
 * {@link ServiceInterfaceProxyGenerator} to generate client proxies for remote
 * services.
 *
 * <p>The generated proxies implement the
 * {@link com.google.appinventor.client.ExtendedServiceProxy} interface
 * and delegate all additional calls to "normal" proxies generated by
 * {@link ServiceInterfaceProxyGenerator}.
 *
 */
public class ExtendedServiceProxyGenerator extends Generator {
  // Suffix that is appended to the name of the service interface to build the
  // name of the proxy class
  private static final String PROXY_SUFFIX = "_ExtendedProxy";

  // Suffix that is appended to the name of the service interface to build the
  // name of the asynchronous service interface
  private static final String ASYNC_SUFFIX = "Async";

  // Delegate generator to generate "normal" proxies
  private static final Generator PROXY_GENERATOR = new ServiceInterfaceProxyGenerator();

  @Override
  public String generate(TreeLogger logger, GeneratorContext context, String typeName)
      throws UnableToCompleteException {
    // Delegate the creation of the "normal" proxy
    String proxyTypeName = PROXY_GENERATOR.generate(logger, context, typeName);

    // Wrap the generated proxy in an extended proxy
    return generateExtendedProxy(logger, context, typeName, proxyTypeName);
  }

  /**
   * Generates a wrapper around the proxy generated by
   * {@link ServiceInterfaceProxyGenerator}.
   *
   * @param logger log interface
   * @param context generator context
   * @param typeName name of the interface that was passed to
   *        {@link com.google.gwt.core.client.GWT#create(Class)}
   * @param proxyTypeName the name of the wrapped proxy class
   * @return the name of the extended proxy class
   */
  private String generateExtendedProxy(TreeLogger logger, GeneratorContext context,
      String typeName, String proxyTypeName) {
    JClassType type = context.getTypeOracle().findType(typeName);
    String packageName = type.getPackage().getName();
    String className = type.getSimpleSourceName() + PROXY_SUFFIX;
    String asyncName = typeName + ASYNC_SUFFIX;

    String classNameExtendedServiceProxy = "com.google.appinventor.client.ExtendedServiceProxy";

    // The generator can be invoked for the same class name more than once.
    // In this case the GeneratorContext.tryCreate method will return null to
    // indicate that the file already exists. This is not an error.
    PrintWriter out = context.tryCreate(logger, packageName, className);
    if (out != null) {
      out.println("package " + packageName + ";");
      out.println("class " + className);
      out.println("    extends " + classNameExtendedServiceProxy + "<" + typeName + ">");
      out.println("    implements " + ServiceDefTarget.class.getName() + ", " + asyncName + " {");
      out.println("  private " + proxyTypeName + " proxy = new " + proxyTypeName + "();");
      out.println("  public String getServiceEntryPoint() {");
      out.println("    return proxy.getServiceEntryPoint();");
      out.println("  }");
      out.println("  public void setRpcRequestBuilder(" + RpcRequestBuilder.class.getName() +
          " builder) {");
      out.println("    proxy.setRpcRequestBuilder(builder);");
      out.println("  }");
      out.println("  public void setServiceEntryPoint(String address) {");
      out.println("    proxy.setServiceEntryPoint(address);");
      out.println("  }");
      out.println("  public String getSerializationPolicyName() {");
      out.println("    return proxy.getSerializationPolicyName();");
      out.println("  }");

      for (JMethod method : type.getMethods()) {
        printMethod(out, method, typeName);
      }

      out.println("}");

      context.commit(logger, out);
    }

    return packageName + "." + className;
  }

  /**
   * Generate the implementation of a single method.
   *
   * @param out where to print the method to
   * @param method the method
   * @param typeName type name of the containing proxy class
   */
  private void printMethod(PrintWriter out, JMethod method, String typeName) {
    // Build parameter lists
    int i = 0;
    StringBuilder actualParamsBuilder = new StringBuilder();
    StringBuilder formalParamsBuilder = new StringBuilder();
    for (JParameter param : method.getParameters()) {
      if (i != 0) {
        actualParamsBuilder.append(", ");
        formalParamsBuilder.append(", ");
      }

      String paramType = param.getType().getParameterizedQualifiedSourceName();
      String paramName = "p" + i++;
      actualParamsBuilder.append(paramName);
      formalParamsBuilder.append(paramType + " " + paramName);
    }
    String actualParams = actualParamsBuilder.toString();
    String formalParams = formalParamsBuilder.toString();

    // Information about the return type
    JType returnType = method.getReturnType();
    boolean hasReturnValue = !returnType.getSimpleSourceName().equals("void");

    JPrimitiveType primitiveReturnType = returnType.isPrimitive();
    String resultType =
        primitiveReturnType != null ? primitiveReturnType.getQualifiedBoxedSourceName()
            : returnType.getParameterizedQualifiedSourceName();

    String callbackType = AsyncCallback.class.getName() + "<" + resultType + ">";

    // Print method
    out.println("  public void " + method.getName() + "(" + formalParams
        + (formalParams.isEmpty() ? "" : ", ") + "final " + callbackType + " callback" + ") {");
    out.println("    fireStart(\"" + method.getName() + "\"" + (actualParams.isEmpty() ? "" : ", ")
        + actualParams + ");");
    out.println("    proxy." + method.getName() + "(" + actualParams
        + (actualParams.isEmpty() ? "" : ", ") + "new " + callbackType + "() {");
    out.println("      public void onSuccess(" + resultType + " result) {");
    out.println("        " + outcome(method, "Success", "result"));
    out.println("      }");
    out.println("      public void onFailure(Throwable caught) {");
    out.println("        " + outcome(method, "Failure", "caught"));
    out.println("      }");
    out.println("    });");
    out.println("  }");
  }

  /**
   * Generate code to handle the outcome of an RPC.
   *
   * @param method the RPC method that was called
   * @param outcome the outcome: "Success" or "Failure"
   * @param result the result of the RPC call or null for void methods
   * @return the generated code
   */
  private String outcome(JMethod method, String outcome, String result) {
    String callListener = "fire" + outcome + "(\"" + method.getName() + "\", " + result + ");";
    String callCallback = "callback.on" + outcome + "(" + result + ");";

    return callListener + ' ' + callCallback;
  }
}
