/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.es.wrapper;

import com.caucho.es.ESArrayWrapper;
import com.caucho.es.ESBase;
import com.caucho.es.ESBeanWrapper;
import com.caucho.es.Global;
import com.caucho.java.JavaCompiler;
import com.caucho.loader.SimpleLoader;
import com.caucho.log.Log;
import com.caucho.server.util.CauchoSystem;
import com.caucho.util.CharBuffer;
import com.caucho.util.IntMap;
import com.caucho.vfs.JarPath;
import com.caucho.vfs.MergePath;
import com.caucho.vfs.Path;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;

import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

public class Wrapper {
  private static final Integer LOCK = new Integer(0);
  private static final Logger log = Log.open(Wrapper.class);
  
  private String name;
  private String javaClassName;
  private Class cl;
  private boolean isPublic;
  private Path dest;
  private WriteStream os;
  private ClassLoader loader;
  private JavaCompiler compiler;

  private ESBeanInfo beanInfo;
  private IntMap hasDispatch;
  private IntMap staticHasDispatch;
  private IntMap setDispatch;
  private IntMap staticSetDispatch;
  private IntMap methodDispatch;
  private IntMap staticMethodDispatch;
  private HashMap namedProperties;
  
  private ArrayList overloadDispatch;
  
  private int depth;
  private boolean isNewline;
  private Class esBase;

  /**
   * Creates the instance of the wrapper generator.
   *
   * @param resin the global parent object
   * @param cl the class to wrap.
   */
  private Wrapper(Global resin, Class cl)
  {
    name = cl.getName().replace('/', '.');

    MergePath mergePath = new MergePath();
    mergePath.addClassPath(cl.getClassLoader());
    Path destClass = mergePath.lookup(name.replace('.', '/') + ".class");

    // technically, need to resort to dynamic.  This is a cheat.
    if (! destClass.exists() && cl.getInterfaces().length > 0) {
      cl = cl.getInterfaces()[0];
      name = cl.getName().replace('/', '.');
    }
    
    javaClassName = toJavaClassName(name);

    CharBuffer cb = new CharBuffer();
    for (int i = 0; i < name.length(); i++) {
      char ch = name.charAt(i);

      if (ch == '$')
        cb.append("_0");
      else if (ch == '_')
        cb.append("__");
      else
        cb.append(ch);
    }
      
    name = "_jsbean." + cb + "_es";

    this.cl = cl;

    isPublic =  Modifier.isPublic(cl.getModifiers());

    //this.loader = resin.getParentLoader();
    loader = cl.getClassLoader();

    compiler = JavaCompiler.create(loader);
    //compiler.setEncoding("utf8");
    Path workPath = CauchoSystem.getWorkPath();
    
    dest = workPath.lookup(name.replace('.', '/') + ".java");

    hasDispatch = new IntMap();
    staticHasDispatch = new IntMap();
    setDispatch = new IntMap();
    staticSetDispatch = new IntMap();
    methodDispatch = new IntMap();
    staticMethodDispatch = new IntMap();
    namedProperties = new HashMap();
    
    overloadDispatch = new ArrayList();

    try {
      esBase = Class.forName("com.caucho.es.ESBase");
    } catch (Exception e) {
    }
  }

  public static ESBase []bean(Global resin, Class cl)
    throws Throwable
  {
    Wrapper wrapper = null;
    
    if (cl.isArray()) {
      ESBase arrayWrapper = ESArrayWrapper.wrapper(resin, cl);
      return new ESBase[] { arrayWrapper.getProperty("CONSTRUCTOR"),
                            arrayWrapper };
    }

    synchronized (LOCK) {
      wrapper = new Wrapper(resin, cl);
      ESBeanWrapper beanWrapper = wrapper.wrap();
      beanWrapper.n = 0;
      
      return new ESBase[] { beanWrapper.wrapStatic(), beanWrapper };
    }
  }

  /**
   * Creates the wrapper for a class
   */
  private ESBeanWrapper wrap() throws Throwable
  {
    dest.getParent().mkdirs();

    Path workPath = CauchoSystem.getWorkPath();
    Path destClass = workPath.lookup(name.replace('.', '/') + ".class");

    ClassLoader beanLoader;
    beanLoader = SimpleLoader.create(loader,
				     CauchoSystem.getWorkPath());
    ESBeanWrapper wrapper;

    try {
      Class cl = CauchoSystem.loadClass(name, false, beanLoader);
      wrapper = (ESBeanWrapper) cl.newInstance();

      if (! wrapper.isModified() && wrapper.getVersionId() == CauchoSystem.getVersionId())
	return wrapper;
    } catch (Throwable e) {
    }
    
    destClass.remove();
    os = dest.openWrite();

    beanInfo = ESIntrospector.getBeanInfo(cl);

    try {
      printHeader();
      printConstructors();
      printHasProperty();
      printSetProperty();
      printKeys();
      printDeletes();
      printMethods();
      printInit();
      printFooter();
    } finally {
      os.close();
    }

    compiler.compile(name.replace('.', '/') + ".java", null);

    beanLoader = SimpleLoader.create(loader,
				     CauchoSystem.getWorkPath());
    
    try {
      Class cl = CauchoSystem.loadClass(name, false, beanLoader);
      wrapper = (ESBeanWrapper) cl.newInstance();
    } catch (NoClassDefFoundError e) {
      e.printStackTrace();
      throw e;
    }

    return wrapper;
  }

  private long getSourceLastModified(Class cl)
  {
    long lastModified = 0;
    String classPath;
    
    URL resource = null;
    String clName = cl.getName().replace('.', '/') + ".class";
    String name;

    if (loader != null)
      resource = loader.getResource(clName);

    String fileName = resource != null ? resource.toExternalForm() : null;

    // XXX: need to implement jar: filesystem
    if (resource == null ||
        fileName.startsWith("systemresource:") ||
        fileName.startsWith("jar:"))
      return getClassPathLastModified(cl);

    Path path = Vfs.lookup(fileName);

    if (path != null && path.canRead())
      return path.getLastModified();
    else
      return 0;
  }

  private long getClassPathLastModified(Class cl)
  {
    String clName = cl.getName().replace('.', '/') + ".class";

    String classPath = System.getProperty("java.class.path");
        
    char sep = CauchoSystem.getPathSeparatorChar();
    int head = 0;
    int tail;
    for (; (tail = classPath.indexOf(sep, head)) >= 0; head = tail + 1) {
      String name = classPath.substring(head, tail);
      Path path = Vfs.lookupNative(name);

      if (name.endsWith(".jar") || name.endsWith(".zip"))
        path = JarPath.create(path);

      if (path != null && path.lookup(clName).canRead()) {
        return path.lookup(clName).getLastModified();
      }
    }

    String name = classPath.substring(head);
    Path path = Vfs.lookupNative(name);

    if (name.endsWith(".jar") || name.endsWith(".zip"))
      path = JarPath.create(path);

    if (path != null && path.lookup(clName).canRead())
      return path.lookup(clName).getLastModified();
    
    return 0;
  }

  private void printHeader() throws IOException
  {
    int p = name.lastIndexOf('.');
    String pkg = name.substring(0, p);
    String clName = name.substring(p + 1);

    println("package " + pkg + ";");
    println("import com.caucho.es.*;");
    Iterator iter = beanInfo.getNonPkgClasses().iterator();
    while (iter.hasNext()) {
      String name = (String) iter.next();
      println("import " + name + ";");
    }
    println();
    println("public class " + clName + " extends ESBeanWrapper {");
    pushDepth();
    if (isPublic)
      println("private " + javaClassName + " _value;");

    println();
    println("public long getVersionId() { return " +
            CauchoSystem.getVersionId() + "L; }");
    
    println();
    println("protected ESBeanWrapper dup()");
    println("{");
    println("  return new " + clName + "();");
    println("}");

    println();
    println("public ESBeanWrapper wrap(Object value)");
    println("{");
    pushDepth();
    println("if (value == null) throw new NullPointerException();");
    println(name + " child = new " + name + "();");
    println("child.value = value;");
    if (isPublic)
      println("child._value = (" + javaClassName + ") value;");
    println("child.hasDispatch = instanceHasDispatch;");
    println("child.setDispatch = instanceSetDispatch;");
    println("child.methodDispatch = instanceMethodDispatch;");
    println("child.n = -1;");
    println("return child;");
    popDepth();
    println("}");
    
    println();
    println("public ESBeanWrapper wrapStatic()");
    println("{");
    pushDepth();
    println(name + " child = new " + name + "();");
    println("child.hasDispatch = staticHasDispatch;");
    println("child.setDispatch = staticSetDispatch;");
    println("child.methodDispatch = staticMethodDispatch;");
    println("child.n = -2;");
    println("child.name = \"" + javaClassName + "\";");
    println("try {");
    println("  child.value = Class.forName(child.name);");
    println("} catch (Exception e) {}");
    println("return child;");
    popDepth();
    println("}");

    println();
    println("public Class getJavaType()");
    println("{");
    pushDepth();
    println("return value.getClass();");
    popDepth();
    println("}");
  }
  
  private void printConstructors() throws IOException
  {
    println();
    println("public ESBase construct(Call call, int length)");
    println("  throws Throwable");
    println("{");
    pushDepth();
    println("if (n != -2)");
    println("  throw new ESException(\"can't create `" + javaClassName + "'\");");
    println();

    if (printMethodConstructor()) {
      popDepth();
      println("}");
      return;
    }
    
    ArrayList overload = beanInfo.getConstructors();
    if (Modifier.isAbstract(cl.getModifiers()))
      overload = null;
    
    if (overload == null || overload.size() == 0) {
      println("  throw new ESException(\"can't create `" + javaClassName + "'\");");
    }
    else {

    Constructor last = null; 
    for (int i = 0; i < overload.size(); i++) {
      if (overload.get(i) instanceof Constructor)
        last = (Constructor) overload.get(i);
    }

    for (int i = 0; i < overload.size(); i++) {
      Object o = overload.get(i);
      if (! (o instanceof Constructor))
        continue;

      Constructor constructor = (Constructor) o;
      if (constructor != last) {
        println("if (length <= " + i + ")");
        print("  ");
      }

      print("return wrap(new " + javaClassName + "(");
      Class []param = constructor.getParameterTypes();
      for (int j = 0; j < param.length; j++) {
        if (j > 0)
          print(", ");

        printArgToJava(param[j], j);
      }
      println("));");
    }
    
    if (last == null)
      println("throw new ESException(\"can't create `" + javaClassName + "'\");");
    }

    popDepth();
    println("}");
  }

  private boolean printMethodConstructor() throws IOException
  {
    ArrayList overload = (ArrayList) beanInfo._staticMethodMap.get("create");
    if (overload != null) {
      printMethod(Integer.MIN_VALUE, "create", overload, null);
      return true;
    }
    else
      return false;
  }

  /**
   * Print the code for accessing properties.
   */
  private void printHasProperty() throws IOException
  {
    println();
    println("public ESBase hasProperty(ESString name)");
    println("  throws Throwable");
    println("{");
    pushDepth();
    println("ESBase temp;");
    println("switch (hasDispatch.get(name)) {");
    
    PropertyDescriptor []props = beanInfo.getPropertyDescriptors();

    int index = 1;
    for (int i = 0; i < props.length; i++) {
      if (props[i] instanceof NamedPropertyDescriptor)
        index = doHasNamedProperty(index, (NamedPropertyDescriptor) props[i]);
      else if (props[i] instanceof ESIndexedPropertyDescriptor)
        index = doHasIndexProperty(index,
                                   (ESIndexedPropertyDescriptor) props[i]);
      else if (props[i] instanceof ESPropertyDescriptor)
        index = doHasProperty(index, (ESPropertyDescriptor) props[i]);
      else
        throw new RuntimeException();
    }
    println("default:");
    println("  return ESBase.esEmpty;");
    println("}");
    popDepth();
    println("}");
  }

  private int doHasIndexProperty(int i, ESIndexedPropertyDescriptor prop)
    throws IOException
  {
    Named named = new Named(prop.getName(), namedProperties.size());
    int n = named.n;
    
    namedProperties.put(prop.getName(), named);
    hasDispatch.put(prop.getName(), i);

    println("case " + i + ":");
    pushDepth();
    println("if (name" + n + " == null) {");
    println("  name" + n + " = new " + name + "();");
    println("  name" + n + ".value = value;");
    if (isPublic)
      println("  name" + n + "._value = _value;");
    println("  name" + n + ".hasDispatch = has" + n + ";");
    println("  name" + n + ".setDispatch = set" + n + ";");
    println("  name" + n + ".delId = " + n + ";");
    println("}");
    println("return name" + n + ";");
    popDepth();

    i += 1;

    ESMethodDescriptor md = prop.getESReadMethod();
    if (md == null)
      return i;

    println("case " + i + ":");
    pushDepth();
    named.get = i;
    
    ESMethodDescriptor size = prop.getESSizeMethod();

    if (size != null) {
      println("if (name.equals(LENGTH)) {");
      pushDepth();
      Method method = size.getMethod();
      Class resultClass = method.getReturnType();
      print("return ");
      startJavaToES(resultClass);
      startProp(size);
      print(")");
      endJavaToES(resultClass);
      println(";");
      popDepth();
      println("} else {");
      pushDepth();
    }
    Method method = md.getMethod();
    Class resultClass = method.getReturnType();
    print("return ");
    startJavaToES(resultClass);
    int p = startProp(md);
    if (p > 0)
      print(", ");
    print("name.toInt32())");
    endJavaToES(resultClass);
    println(";");
    if (size != null) {
      popDepth();
      println("}");
    }

    popDepth();

    return i + 1;
  }

  private int doHasNamedProperty(int i, NamedPropertyDescriptor prop)
    throws IOException
  {
    Named named = new Named(prop.getName(), namedProperties.size());
    int n = named.n;
    
    namedProperties.put(prop.getName(), named);
    hasDispatch.put(prop.getName(), i);

    println("case " + i + ":");
    pushDepth();
    println("if (name" + n + " == null) {");
    println("  name" + n + " = new " + name + "();");
    println("  name" + n + ".value = value;");
    if (isPublic)
      println("  name" + n + "._value = _value;");
    println("  name" + n + ".hasDispatch = has" + n + ";");
    println("  name" + n + ".setDispatch = set" + n + ";");
    println("  name" + n + ".delId = " + n + ";");
    println("}");
    println("return name" + n + ";");
    popDepth();

    i += 1;

    ESMethodDescriptor md = prop.getNamedReadMethod();
    if (md == null)
      return i;

    println("case " + i + ":");
    pushDepth();
    named.get = i;

    Method method = md.getMethod();
    if (Modifier.isStatic(method.getModifiers()) && ! md.isStaticVirtual())
      staticHasDispatch.put(prop.getName(), i - 1);
    
    Class resultClass = method.getReturnType();
    print("return ");
    startJavaToES(resultClass);
    int p = startProp(md);
    if (p > 0)
      print(", ");
    print("name.toJavaString())");
    endJavaToES(resultClass);
    println(";");
    popDepth();
 
    return i + 1;
  }

  private int doHasProperty(int i, ESPropertyDescriptor prop)
    throws IOException
  {
    Field field = prop.getESField();
    ESMethodDescriptor md = prop.getESReadMethod();

    if (field != null && ! Modifier.isPublic(field.getModifiers()))
      field = null;

    if (md == null && field == null)
      return i;
    
    hasDispatch.put(prop.getName(), i);
    
    println("case " + i + ":");
    pushDepth();

    if (field != null) {
      Class resultClass = field.getType();
      print("return ");
      startJavaToES(resultClass);
      if (isPublic && field.getDeclaringClass().getName().equals(cl.getName()))
        print("_value.");
      else if (Modifier.isStatic(field.getModifiers()))
        print(toJavaClassName(field.getDeclaringClass().getName()) + ".");
      else
        print("((" + toJavaClassName(field.getDeclaringClass().getName()) + ") value).");
      print(field.getName());
      endJavaToES(resultClass);
      println(";");
      popDepth();
      
      if (Modifier.isStatic(field.getModifiers()))
        staticHasDispatch.put(prop.getName(), i);
      
      return i + 1;
    }
    
    Method method = md.getMethod();
    
    if (Modifier.isStatic(method.getModifiers()) && ! md.isStaticVirtual())
      staticHasDispatch.put(prop.getName(), i);
      
    print("return ");

    Class resultClass = method.getReturnType();
    
    startJavaToES(resultClass);
    int p = startProp(md);
    print(")");
    endJavaToES(resultClass);
    println(";");
    popDepth();

    return i + 1;
  }
  
  private void printSetProperty() throws IOException
  {
    println();
    println("public void setProperty(ESString name, ESBase newValue)");
    println("  throws Throwable");
    println("{");
    pushDepth();
    println("ESBase temp;");
    println("switch (setDispatch.get(name)) {");
    
    PropertyDescriptor []props = beanInfo.getPropertyDescriptors();

    int index = 0;
    for (int i = 0; i < props.length; i++) {
      if (props[i] instanceof NamedPropertyDescriptor) {
        index = doSetNamedProperty(index,
                                   (NamedPropertyDescriptor) props[i]);
      }
      else if (props[i] instanceof ESIndexedPropertyDescriptor) {
        index = doSetIndexProperty(index,
                                   (ESIndexedPropertyDescriptor) props[i]);
      }
      else if (props[i] instanceof ESPropertyDescriptor)
        index = doSetProperty(index, (ESPropertyDescriptor) props[i]);
      else
        throw new RuntimeException();
    }
    println("default:");
    println("  return;");
    println("}");
    popDepth();
    println("}");
  }

  private int doSetNamedProperty(int i, NamedPropertyDescriptor prop)
    throws IOException
  {
    Named named = (Named) namedProperties.get(prop.getName());
    if (named == null)
      return i;
    
    int n = named.n;

    ESMethodDescriptor md = prop.getNamedWriteMethod();
    if (md == null)
      return i;

    println("case " + i + ":");
    pushDepth();
    named.set = i;
    
    int p = startProp(md);
    Class []param = md.getParameterTypes();
    
    if (p != 0)
      print(", ");
    print("name.toJavaString(), ");
    printValueToJava(param[1], "newValue");
    println(");");

    println("return;");
    popDepth();

    return i + 1;
  }

  private int doSetIndexProperty(int i, ESIndexedPropertyDescriptor prop)
    throws IOException
  {
    Named named = (Named) namedProperties.get(prop.getName());
    if (named == null)
      return i;
    
    int n = named.n;

    ESMethodDescriptor md = prop.getESWriteMethod();
    if (md == null)
      return i;

    println("case " + i + ":");
    pushDepth();
    named.set = i;
    
    int p = startProp(md);
    Class []param = md.getParameterTypes();
    
    if (p != 0)
      print(", ");
    print("name.toInt32(), ");
    printValueToJava(param[1], "newValue");
    println(");");

    println("return;");
    popDepth();

    return i + 1;
  }

  private int doSetProperty(int i, ESPropertyDescriptor prop)
    throws IOException
  {
    ESMethodDescriptor md = prop.getESWriteMethod();
    Field field = prop.getESField();
    if (field != null && Modifier.isFinal(field.getModifiers()))
      field = null;

    if (md == null && field == null)
      return i;

    println("case " + i + ":");
    pushDepth();
    
    setDispatch.put(prop.getName(), i);

    if (field != null) {
      Class resultClass = field.getType();
      if (isPublic)
        print("_value.");
      else
        print("((" + field.getDeclaringClass().getName() + ") value).");
      print(field.getName());
      print(" = ");
      printValueToJava(resultClass, "newValue");
      println(";");
      println("return;");
      popDepth();
      return i + 1;
    }
    
    Method method = md.getMethod();
    
    if (Modifier.isStatic(method.getModifiers()) && ! md.isStaticVirtual())
      staticSetDispatch.put(prop.getName(), i);
    
    Class []param = md.getParameterTypes();
    
    int p = startProp(md);
    if (p != 0)
      print(", ");
    printValueToJava(param[0], "newValue");
    println(");");

    println("return;");
    
    popDepth();

    return i + 1;
  }
  
  private void printKeys() throws IOException
  {
    println();
    println("public java.util.Iterator keys()");
    println("  throws Throwable");
    println("{");
    pushDepth();
    println("switch (delId) {");

    ESMethodDescriptor md = beanInfo.iterator;
    if (md != null) {
      println("case -1:");
      print("  return Call.toESIterator(");
      startProp(md);
      println("));");
    }

    PropertyDescriptor []props = beanInfo.getPropertyDescriptors();
    for (int i = 0; i < props.length; i++) {
      if (props[i] instanceof NamedPropertyDescriptor)
        printNamedKey((NamedPropertyDescriptor) props[i]);
    }
    
    println("default:");
    println("  return super.keys();");
    println("}");
    popDepth();
    println("}");
  }

  private void printNamedKey(NamedPropertyDescriptor prop)
    throws IOException
  {
    ESMethodDescriptor md = prop.getNamedIteratorMethod();
    if (md == null)
      return;

    Named named = (Named) namedProperties.get(prop.getName());
    println("case " + named.n + ":");
    pushDepth();
    print("return Call.toESIterator(");
    int p = startProp(md);
    println("));");
    popDepth();
  }
  
  private void printDeletes() throws IOException
  {
    println();
    println("public ESBase delete(ESString key)");
    println("  throws Throwable");
    println("{");
    pushDepth();
    println("switch (delId) {");

    PropertyDescriptor []props = beanInfo.getPropertyDescriptors();
    for (int i = 0; i < props.length; i++) {
      if (props[i] instanceof NamedPropertyDescriptor)
        printNamedDelete((NamedPropertyDescriptor) props[i]);
    }
    
    println("default:");
    println("  return ESBoolean.FALSE;");
    println("}");
    popDepth();
    println("}");
  }

  private void printNamedDelete(NamedPropertyDescriptor prop)
    throws IOException
  {
    ESMethodDescriptor md = prop.getNamedRemoveMethod();
    if (md == null)
      return;

    Named named = (Named) namedProperties.get(prop.getName());
    println("case " + named.n + ":");
    pushDepth();
    int p = startProp(md);
    if (p > 0)
      print(", ");
    println("key.toJavaString());");
    println("return ESBoolean.TRUE;");
    popDepth();
  }

  /**
   * Prints all the accessible methods in this object.
   */
  private void printMethods() throws IOException
  {
    println();
    println("public ESBase call(Call call, int length, int n)");
    println("  throws Throwable");
    println("{");
    pushDepth();
    println("ESBase temp;");
    println("switch (n) {");
    
    ArrayList overload = (ArrayList) beanInfo._methodMap.get("call");
    if (overload != null)
      printMethod(-1, "call", overload, null);

    // Print the constructor (code -2)
    ArrayList create = (ArrayList) beanInfo._staticMethodMap.get("create");
    ArrayList call = (ArrayList) beanInfo._staticMethodMap.get("call");
    if (create != null)
      printMethod(-2, "create", create, null);
    else if (call != null)
      printMethod(-2, "call", create, null);
    else {
      println("case -2:");
      println("  return construct(call, length);");
    }
    
    Iterator iter = beanInfo._methodMap.entrySet().iterator();
    int i = 0;
    while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      overload = (ArrayList) entry.getValue();
      String name = (String) entry.getKey();

      i = printMethod(i, name, overload, methodDispatch);
    }
    
    iter = beanInfo._staticMethodMap.entrySet().iterator();
    while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      overload = (ArrayList) entry.getValue();
      String name = (String) entry.getKey();

      i = printMethod(i, name, overload, staticMethodDispatch);
    }

    println("}");
    println("return ESBase.esUndefined;");
    popDepth();
    println("}");
  }

  /**
   * Prints a method in a method dispatch.
   */
  private int printMethod(int i, String name, ArrayList overload,
                          IntMap dispatch)
    throws IOException
  {
    ESMethodDescriptor []last = null;

    if (overload == null)
      return i;
    
    for (int j = 0; j < overload.size(); j++) {
      last = (ESMethodDescriptor []) overload.get(j);
    }

    if (last == null) {
      return i;
    }

    if (i > -100) {
      println("case " + i + ":");
      pushDepth();
      if (dispatch != null)
        dispatch.put(name, i++);
    }

    if (overload.size() > 2) {
      ESMethodDescriptor []mds = (ESMethodDescriptor []) overload.get(2);

      for (int j = 0; mds != null && j < mds.length; j++) {
        Class []cl = mds[j].getParameterTypes();
        int p = cl.length - 2;

        if (cl[0].getName().equals("com.caucho.es.Call") &&
            cl[1].getName().equals("int")) {
          printMethod(mds[j], dispatch == null);
          popDepth();
          return i;
        }
      }
    }

    for (int j = 0; j < overload.size(); j++) {
      Object o = overload.get(j);

      if (o == null)
        continue;

      ESMethodDescriptor []mds = (ESMethodDescriptor []) o;
      if (mds != last) {
        println("if (length <= " + j + ") {");
        pushDepth();
      }

      if (mds.length == 1)
        printMethod(mds[0], dispatch == null);
      else {
        String var = "dispatch" + overloadDispatch.size();
        
        overloadDispatch.add(mds);

        println("switch (" + var + ".select(call, length)) {");
        for (int k = 0; k < mds.length; k++) {
          println("case " + k + ":");
          pushDepth();
          printMethod(mds[k], dispatch == null);
          popDepth();
        }

        println("default:");
        println("  throw new ESException(\"no matching method " + mds[0].getName() + "\");");
        println("}");
      }
      
      if (mds != last) {
        popDepth();
        println("}");
      }
    }

    if (i > -100) {
      popDepth();
    }
    
    return i;
  }

  /**
   * Print a single method based on a method descriptor.  The arguments
   * are assumed to come from a Call object.
   *
   * @param md the method descriptor.
   */
  private void printMethod(ESMethodDescriptor md, boolean isProp)
    throws IOException
  {
    boolean hasThrowable = hasException(md.getMethod().getExceptionTypes(),
                                        Throwable.class);

    /*
    if (hasThrowable) {
      println("try {");
      pushDepth();
    }
    */
    
    Class returnCl = md.getReturnType();
    if (! returnCl.getName().equals("void")) {
      print("return ");
      startJavaToES(returnCl);
    }

    Class []param = md.getParameterTypes();
    
    int p;

    if (isProp)
      p = startProp(md);
    else
      p = startCall(md);

    if (param.length == 2 &&
        param[0].getName().equals("com.caucho.es.Call") &&
        param[1].getName().equals("int")) {
      if (p > 0)
        print(", ");
      print("call, length");
    }
    else {
      for (int j = 0; j < param.length; j++) {
        if (j + p > 0)
          print(", ");
      
        printArgToJava(param[j], j);
      }
    }

    if (returnCl.getName().equals("void")) {
      println(");");
      println("return ESBase.esUndefined;");
    }
    else {
      print(")");
      endJavaToES(returnCl);
      println(";");
    }

    /*
    if (hasThrowable) {
      popDepth();
      println("} catch (Exception e) {");
      println("  throw e;");
      println("} catch (RuntimeException e) {");
      println("  throw e;");
      println("} catch (Error e) {");
      println("  throw e;");
      println("} catch (Throwable e) {");
      println("  throw new com.caucho.es.ESException(String.valueOf(e));");
      println("}");
    }
    */
  }

  private boolean hasException(Class []exn, Class cl)
  {
    for (int i = 0; i < exn.length; i++)
      if (exn[i].isAssignableFrom(cl))
        return true;

    return false;
  }

  /**
   * Starts a method call that overloads a property.
   */
  private int startProp(ESMethodDescriptor md)
    throws IOException
  {
    Method method = md.getMethod();
    
    int p = 0;
    if (md.isStaticVirtual()) {
      print(md.getMethodClassName());
      print(".");
      print(md.getMethod().getName());
      if (isPublic)
        print("(_value");
      else
        print("((" + toJavaClassName(md.getObjectClassName()) + ") value");
      p = 1;
    } else if (Modifier.isStatic(method.getModifiers())) {
      print(md.getMethodClassName());
      print(".");
      print(md.getMethod().getName());
      print("(");
    } else {
      if (isPublic)
        print("_value.");
      else
        print("((" + toJavaClassName(md.getObjectClassName()) + ") value).");
      print(md.getMethod().getName());
      print("(");
    }

    return p;
  }

  private int startCall(ESMethodDescriptor md)
    throws IOException
  {
    Method method = md.getMethod();

    int p = 0;
    if (md.isStaticVirtual()) {
      print(toJavaClassName(md.getMethodClassName()));
      print(".");
      print(md.getMethod().getName());
      print("((" + md.getObjectClassName() + ") call.getThisWrapper()");
      p = 1;
    } else if (Modifier.isStatic(method.getModifiers())) {
      print(toJavaClassName(md.getMethodClassName()));
      print(".");
      print(md.getMethod().getName());
      print("(");
    } else {
      print("((" + toJavaClassName(md.getObjectClassName()) + ") call.getThisWrapper()).");
      print(md.getMethod().getName());
      print("(");
    }

    return p;
  }

  private void startJavaToES(Class cl)
    throws IOException
  {
    String name = cl.getName();
    
    switch (classTypes.get(name)) {
    case T_V: 
      //addJsUndefinedRef();
      break;

    case T_Z:
      print("ESBoolean.create(");
      break;

    case T_C:
      print("ESString.createFromCharCode(");
      break;
      
    case T_B: case T_S: case T_I: case T_L:
    case T_F: case T_D:
      print("ESNumber.create(");
      break;

    case T_STRING:
      print("ESString.toStr(");
      break;

    default:
      if (esBase.isAssignableFrom(cl))
        print("((temp = ");
      else
        print("Global.wrap(");
      break;
    }
  }
  
  private void endJavaToES(Class cl)
    throws IOException
  {
    String name = cl.getName();
    
    switch (classTypes.get(name)) {
    case T_V: 
      //addJsUndefinedRef();
      break;

    case T_Z:
    case T_C:
    case T_B: case T_S: case T_I: case T_L:
    case T_F: case T_D:
    case T_STRING:
      print(")");
      break;

    default:
      if (esBase.isAssignableFrom(cl))
        print(") == null ? ESBase.esNull : temp)");
      else
        print(")");
      break;
    }
  }

  private void printValueToJava(Class cl, String value)
    throws IOException
  {
    String name = cl.getName();
    
    switch (classTypes.get(name)) {
    case T_V: 
      throw new RuntimeException();

    case T_Z:
      print(value + ".toBoolean()");
      break;

    case T_C:
      print("(char) " + value + ".toStr().carefulCharAt(0)");
      break;
      
    case T_B:
      print("(byte) " + value + ".toInt32()");
      break;
      
    case T_S:
      print("(short) " + value + ".toInt32()");
      break;
      
    case T_I:
      print(value + ".toInt32()");
      break;
      
    case T_L:
      print("(long)" + value + ".toNum()");
      break;
      
    case T_F:
      print("(float)" + value + ".toNum()");
      break;
      
    case T_D:
      print(value + ".toNum()");
      break;

    case T_STRING:
      print("(" + value + ").toJavaString()");
      break;
      
    default:
      if (cl.isAssignableFrom(esBase))
        print(value);
      else if (esBase.isAssignableFrom(cl)) {
        print("(");
        printClassType(cl);
        print(") " + value);
      }
      else {
        print("(");
        printClassType(cl);
        print(") " + value + ".toJavaObject()");
      }
      break;
    }
  }

  private void printArgToJava(Class cl, int i)
    throws IOException
  {
    String name = cl.getName();
    
    switch (classTypes.get(name)) {
    case T_V: 
      throw new RuntimeException();

    case T_Z:
      print("call.getArg(" + i + ", length).toBoolean()");
      break;

    case T_C:
      print("(char) call.getArg(" + i + ", length).toStr().carefulCharAt(0)");
      break;
      
    case T_B:
      print("(byte) call.getArgInt32(" + i + ", length)");
      break;
      
    case T_S:
      print("(short) call.getArgInt32(" + i + ", length)");
      break;
      
    case T_I:
      print("call.getArgInt32(" + i + ", length)");
      break;
      
    case T_L:
      print("(long) call.getArgNum(" + i + ", length)");
      break;
      
    case T_F:
      print("(float)call.getArgNum(" + i + ", length)");
      break;
      
    case T_D:
      print("call.getArgNum(" + i + ", length)");
      break;

    case T_STRING:
      print("call.getArgString(" + i + ", length)");
      break;
      
    default:
      if (cl.isAssignableFrom(esBase) &&
          ! cl.getName().equals("java.lang.Object"))
        print("call.getArg(" + i + ", length)");
      else if (esBase.isAssignableFrom(cl)) {
        print("(");
        printClassType(cl);
        print(") ");
        print("call.getArg(" + i + ", length)");
      }
      else {
        print("(");
        printClassType(cl);
        print(") call.getArgObject(" + i + ", length)");
      }
      break;
    }
  }

  private void printClassType(Class cl)
    throws IOException
  {
    if (cl.isArray()) {
      printClassType(cl.getComponentType());
      print("[]");
    }
    else {
      print(cl.getName().replace('$', '.'));
    }
  }

  private void printInit() throws IOException
  {
    println("private int delId = -1;");
    println();
    println("private static com.caucho.util.IntMap instanceHasDispatch;");
    println("private static com.caucho.util.IntMap instanceSetDispatch;");
    println("private static com.caucho.util.IntMap instanceMethodDispatch;");
    println("private static com.caucho.util.IntMap staticMethodDispatch;");
    println("private static com.caucho.util.IntMap staticHasDispatch;");
    println("private static com.caucho.util.IntMap staticSetDispatch;");
    Iterator iter = namedProperties.values().iterator();
    while (iter.hasNext()) {
      Named named = (Named) iter.next();
      println(name + " name" + named.n + ";");
      print("private static ConstIntMap has" + named.n);
      println(" = new ConstIntMap(" + named.get + ");");
      print("private static ConstIntMap set" + named.n);
      println(" = new ConstIntMap(" + named.set + ");");
    }
    
    for (int i = 0; i < overloadDispatch.size(); i++) {
      print("private static com.caucho.es.wrapper.MethodDispatcher dispatch" + i);
      println(" = new com.caucho.es.wrapper.MethodDispatcher(new Class[][] {");
      pushDepth();

      ESMethodDescriptor []mds;
      mds = (ESMethodDescriptor []) overloadDispatch.get(i);

      for (int j = 0; j < mds.length; j++) {
        print("new Class[] {");

        Class []param = mds[j].getParameterTypes();

        for (int k = 0; k < param.length; k++) {
          printClass(param[k]);
          print(", ");
        }
        println("},");
      }
      popDepth();
      println("});");
    }
    
    println();
    println("static { _init(); }");

    println();
    println("public boolean isModified()");
    println("{");
    pushDepth();
    com.caucho.make.ClassDependency dep = new com.caucho.make.ClassDependency(cl);
    println("try {");
    println("  Class cl = Class.forName(\"" + cl.getName() + "\", false, Thread.currentThread().getContextClassLoader());");
    println("  return new com.caucho.make.ClassDependency(cl, " + dep.getDigest() + "L).isModified();");
    println("} catch (Throwable e) {");
    println("  return true;");
    println("}");
    popDepth();
    println("}");
    
    println();
    println("private static void _init()");
    println("{");
    
    pushDepth();
    println("instanceHasDispatch = new com.caucho.util.IntMap();");
    iter = hasDispatch.iterator();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      
      println("instanceHasDispatch.put(ESId.intern(\"" + key + "\"), " +
              hasDispatch.get(key) + ");");
    }

    println();
    println("staticHasDispatch = new com.caucho.util.IntMap();");
    iter = staticHasDispatch.iterator();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      
      println("staticHasDispatch.put(ESId.intern(\"" + key + "\"), " +
              staticHasDispatch.get(key) + ");");
    }
    
    println();
    println("instanceSetDispatch = new com.caucho.util.IntMap();");
    iter = setDispatch.iterator();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      
      println("instanceSetDispatch.put(ESId.intern(\"" + key + "\"), " +
              setDispatch.get(key) + ");");
    }
    
    println();
    println("staticSetDispatch = new com.caucho.util.IntMap();");
    iter = staticSetDispatch.iterator();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      
      println("staticSetDispatch.put(ESId.intern(\"" + key + "\"), " +
              staticSetDispatch.get(key) + ");");
    }
    
    println();
    println("instanceMethodDispatch = new com.caucho.util.IntMap();");
    iter = methodDispatch.iterator();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      
      println("instanceMethodDispatch.put(ESId.intern(\"" + key + "\"), " +
              methodDispatch.get(key) + ");");
    }
    
    println();
    println("staticMethodDispatch = new com.caucho.util.IntMap();");
    iter = staticMethodDispatch.iterator();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      
      println("staticMethodDispatch.put(ESId.intern(\"" + key + "\"), " +
              staticMethodDispatch.get(key) + ");");
    }
    
    popDepth();
    println("}");
  }

  private void printClass(Class cl)
    throws IOException
  {
    if (! cl.isArray()) {
      print(cl.getName() + ".class");
      return;
    }

    print("(new ");
    printArrayClass(cl.getComponentType());
    print("[0]).getClass()");
  }

  private void printArrayClass(Class cl)
    throws IOException
  {
    if (cl.isArray()) {
      printArrayClass(cl.getComponentType());
      print("[]");
    }
    else
      print(cl.getName());
  }
  
  private void printFooter() throws IOException
  {
    popDepth();
    println("}");
  }

  private void pushDepth()
  {
    depth += 2;
  }

  private void popDepth()
  {
    depth -= 2;
  }
    
  private void print(String s) throws IOException
  {
    if (isNewline)
      printDepth();
    os.print(s);
  }

  private void println(String s) throws IOException
  {
    if (isNewline)
      printDepth();
    os.println(s);
    isNewline = true;
  }

  private void println() throws IOException
  {
    if (isNewline)
      printDepth();
    os.println();
    isNewline = true;
  }

  private void printDepth() throws IOException
  {
    for (int i = 0; i < depth; i++)
      os.print(' ');
    isNewline = false;
  }

  private String toJavaClassName(String name)
  {
    CharBuffer cb = CharBuffer.allocate();
    for (int i = 0; i < name.length(); i++) {
      char ch = name.charAt(i);

      if (ch == '$' && i > 0 && name.charAt(i - 1) != '.')
        cb.append(".");
      else
        cb.append(ch);
    }
    return cb.close();
  }    

  static HashMap<String,String> classNames;
  static {
    classNames = new HashMap<String,String>();
    classNames.put("void", "V");
    classNames.put("boolean", "Z");
    classNames.put("byte", "B");
    classNames.put("short", "S");
    classNames.put("char", "C");
    classNames.put("int", "I");
    classNames.put("long", "J");
    classNames.put("float", "F");
    classNames.put("double", "D");
  }

  static IntMap classTypes;
  static final int T_V = 0;
  static final int T_Z = T_V + 1;
  static final int T_B = T_Z + 1;
  static final int T_S = T_B + 1;
  static final int T_C = T_S + 1;
  static final int T_I = T_C + 1;
  static final int T_L = T_I + 1;
  static final int T_F = T_L + 1;
  static final int T_D = T_F + 1;
  static final int T_STRING = T_D + 1;
  static {
    classTypes = new IntMap();
    classTypes.put("void", T_V);
    classTypes.put("boolean", T_Z);
    classTypes.put("byte", T_B);
    classTypes.put("short", T_S);
    classTypes.put("char", T_C);
    classTypes.put("int", T_I);
    classTypes.put("long", T_L);
    classTypes.put("float", T_F);
    classTypes.put("double", T_D);
    classTypes.put("java.lang.String", T_STRING);
  }

  static class Named {
    String name;
    int n;
    int get = -1;
    int set = -1;
    int keys = -1;
    int remove = -1;
    
    Named(String name, int n)
    {
      this.name = name;
      this.n = n;
    }
  }
}

