/*
 * 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;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
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.JType;
import com.google.gwt.i18n.rebind.AbstractResource;
import com.google.gwt.i18n.rebind.AbstractResource.MissingResourceException;
import com.google.gwt.i18n.shared.GwtLocale;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * Abstract functionality needed to create classes needed to supply generators.
 */
public abstract class AbstractGeneratorClassCreator extends
    AbstractSourceCreator {

  /**
   * Returns all interface methods associated with the given type.
   * 
   * @param type associated type
   * @return interface methods.
   */
  public static JMethod[] getAllInterfaceMethods(JClassType type) {
    Map<String, JMethod> methods = new LinkedHashMap<>();
    getAllInterfaceMethodsAux(type, methods);
    return methods.values().toArray(new JMethod[methods.size()]);
  }

  private static void getAllInterfaceMethodsAux(JClassType type,
      Map<String, JMethod> m) {
    if (type.isInterface() != null) {
      JMethod[] methods = type.getMethods();
      for (int i = 0; i < methods.length; i++) {
        String s = uniqueMethodKey(methods[i]);
        if (m.get(s) == null) {
          m.put(s, methods[i]);
        }
      }
      JClassType[] supers = type.getImplementedInterfaces();
      for (int i = 0; i < supers.length; i++) {
        getAllInterfaceMethodsAux(supers[i], m);
      }
    }
  }

  private static String uniqueMethodKey(JMethod method) {
    String name = method.getName();
    name += "(";
    JParameter[] m = method.getParameters();
    for (int i = 0; i < m.length; i++) {
      name += m[i].getType() + " ";
    }
    name += ")";
    return name;
  }

  /**
   * List of registered method factories associated with <code>Constant</code>
   * method implementations.
   */
  protected Map<JType, AbstractMethodCreator> methodFactories = new HashMap<JType, AbstractMethodCreator>();

  /**
   * The interface the generator is conforming to.
   */
  JClassType targetClass;

  private final SourceWriter writer;

  /**
   * Creates a new class creator, supplies a place to write the class, the
   * interface to conform to, and the new name.
   * 
   * @param writer writer
   * @param targetClass class name
   */
  public AbstractGeneratorClassCreator(SourceWriter writer,
      JClassType targetClass) {
    this.targetClass = targetClass;
    this.writer = writer;
  }

  /**
   * Emits the new class.
   * 
   * @param logger
   * @param locale 
   * @throws UnableToCompleteException
   */
  public void emitClass(TreeLogger logger, GwtLocale locale)
      throws UnableToCompleteException {
    logger = branch(logger, branchMessage());
    classPrologue();
    emitMethods(logger, targetClass, locale);
    classEpilog();
    getWriter().outdent();
    getWriter().println("}");
  }

  public JClassType getTarget() {
    return targetClass;
  }

  public UnableToCompleteException logMissingResource(TreeLogger logger, String during,
      MissingResourceException e) {
    String msg = "No resource found for key '" + e.getKey() + "'";
    if (during != null) {
      msg += " while " + during;
    }
    logger.log(TreeLogger.ERROR, msg, e);
    TreeLogger searchedBranch = logger.branch(TreeLogger.WARN, "Searched the following resources:",
        null);
    for (AbstractResource resource : e.getSearchedResources()) {
      TreeLogger resBranch = searchedBranch.branch(TreeLogger.WARN, resource.toString(), null);
      Set<String> keys = resource.keySet();
      TreeLogger keyBranch = resBranch.branch(TreeLogger.INFO, "List of keys found", null);
      if (keyBranch.isLoggable(TreeLogger.INFO)) {
        for (String key : keys) {
          keyBranch.log(TreeLogger.INFO, key, null);
        }
      }
    }
    return new UnableToCompleteException();
  }

  /**
   * Registers a method creator.
   * 
   * @param returnType return type that this creator handles.
   * @param creator creator to register
   */
  public void register(JType returnType, AbstractMethodCreator creator) {
    // TODO: Hacked to get the gwt-trunk for 1.5 building.
    methodFactories.put(returnType.getErasedType(), creator);
  }

  /**
   * Returns the standard message when constructing a branch.
   * 
   * @return branch message
   */
  protected String branchMessage() {
    return "Constructing " + targetClass;
  }

  /**
   * Entry point for subclass cleanup code.
   */
  protected void classEpilog() {
  }

  /**
   * Entry point for subclass setup code.
   */
  protected void classPrologue() {
  }

  /**
   * Emit method body, arguments are arg1...argN.
   * 
   * @param logger TreeLogger for logging
   * @param method method to generate
   * @param locale locale for this generation
   * @throws UnableToCompleteException
   */
  protected abstract void emitMethodBody(TreeLogger logger, JMethod method,
      GwtLocale locale) throws UnableToCompleteException;

  /**
   * Gets the method creator associated with the return type of the method.
   * 
   * @param logger
   * @param method method to create
   * @return the method creator
   * @throws UnableToCompleteException
   */
  protected AbstractMethodCreator getMethodCreator(TreeLogger logger,
      JMethod method) throws UnableToCompleteException {
    JType type = method.getReturnType();
    
    // TODO make the build work.  This is not correct.
    type = type.getErasedType();
    
    AbstractMethodCreator methodCreator = methodFactories.get(type);
    if (methodCreator == null) {
      String msg = "No current method creator exists for " + method
          + " only methods with return types of ";
      Iterator<JType> iter = this.methodFactories.keySet().iterator();
      while (iter.hasNext()) {
        msg += iter.next().getSimpleSourceName();
        if (iter.hasNext()) {
          msg += ", ";
        }
      }
      msg += " can be created";
      throw error(logger, msg);
    }
    return methodCreator;
  }

  /**
   * Gets the associated writer.
   * 
   * @return writer
   */
  protected SourceWriter getWriter() {
    return writer;
  }

  private void emitMethods(TreeLogger logger, JClassType cur, GwtLocale locale)
      throws UnableToCompleteException {
    JMethod[] x = getAllInterfaceMethods(cur);
    for (int i = 0; i < x.length; i++) {
      getWriter().println();
      genMethod(logger, x[i], locale);
    }
  }

  /**
   * Generates a method declaration for the given method.
   * 
   * @param method method to generate
   * @param locale 
   * @throws UnableToCompleteException
   */
  private void genMethod(TreeLogger logger, JMethod method, GwtLocale locale)
      throws UnableToCompleteException {
    String name = method.getName();
    String returnType = method.getReturnType().getParameterizedQualifiedSourceName();
    getWriter().print("public " + returnType + " " + name + "(");
    JParameter[] params = method.getParameters();
    for (int i = 0; i < params.length; i++) {
      if (i != 0) {
        getWriter().print(",");
      }
      if (method.isVarArgs() && i == params.length - 1) {
        JArrayType arrayType = params[i].getType().isArray();
        getWriter().print(
            arrayType.getComponentType().getParameterizedQualifiedSourceName()
            + "... arg" + (i));
      } else {
        getWriter().print(
            params[i].getType().getParameterizedQualifiedSourceName() + " arg"
                + (i));
      }
    }
    getWriter().println(") {");
    getWriter().indent();
    String methodName = method.getName();
    TreeLogger branch = branch(logger, "Generating method body for "
        + methodName + "()");
    try {
      emitMethodBody(branch, method, locale);
    } catch (MissingResourceException e) {
      throw logMissingResource(branch, null, e);
    }
    getWriter().outdent();
    getWriter().println("}");
  }
}
