/* ClassRmicCompiler.java --
   Copyright (c) 1996, 1997, 1998, 1999, 2001, 2002, 2003, 2004, 2005
   Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath 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, or (at your option)
any later version.

GNU Classpath 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.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA. */

package gnu.classpath.tools.rmic;

import gnu.java.rmi.server.RMIHashes;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.MarshalException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.UnexpectedException;
import java.rmi.UnmarshalException;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteRef;
import java.rmi.server.RemoteStub;
import java.rmi.server.Skeleton;
import java.rmi.server.SkeletonMismatchException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Label;
import org.objectweb.asm.Type;

public class ClassRmicCompiler
  implements RmicBackend
{
  private String[] args;
  private int next;
  private List errors = new ArrayList();
  private boolean keep = false;
  private boolean need11Stubs = true;
  private boolean need12Stubs = true;
  private boolean compile = true;
  private boolean verbose;
  private boolean noWrite;
  private String destination;
  private String classpath;
  private ClassLoader loader;
  private int errorCount = 0;

  private Class clazz;
  private String classname;
  private String classInternalName;
  private String fullclassname;
  private MethodRef[] remotemethods;
  private String stubname;
  private String skelname;
  private List mRemoteInterfaces;

  /**
   * @return true if run was successful
   */
  public boolean run(String[] inputFiles)
  {
    args = inputFiles;

    if (next >= args.length)
      return false;

    for (int i = next; i < args.length; i++)
      {
	try
	  {
            if (verbose)
	      System.out.println("[Processing class " + args[i] + ".class]");
	    processClass(args[i].replace(File.separatorChar, '.'));
	  }
        catch (IOException e)
          {
            errors.add(e);
          }
        catch (RMICException e)
          {
            errors.add(e);
          }
      }
    if (errors.size() > 0)
      {
        for (Iterator it = errors.iterator(); it.hasNext(); )
          {
            Exception ex = (Exception) it.next();
            logError(ex);
          }
      }

    return errorCount == 0;
  }

  private void processClass(String cls) throws IOException, RMICException
  {
    // reset class specific vars
    clazz = null;
    classname = null;
    classInternalName = null;
    fullclassname = null;
    remotemethods = null;
    stubname = null;
    skelname = null;
    mRemoteInterfaces = new ArrayList();

    analyzeClass(cls);
    generateStub();
    if (need11Stubs)
      generateSkel();
  }

  private void analyzeClass(String cname)
    throws RMICException
  {
    if (verbose)
      System.out.println("[analyze class " + cname + "]");
    int p = cname.lastIndexOf('.');
    if (p != -1)
      classname = cname.substring(p + 1);
    else
      classname = cname;
    fullclassname = cname;

    findClass();
    findRemoteMethods();
  }

  /**
   * @deprecated
   */
  public Exception getException()
  {
    return errors.size() == 0 ? null : (Exception) errors.get(0);
  }

  private void findClass()
    throws RMICException
  {
    ClassLoader cl = (loader == null
                      ? ClassLoader.getSystemClassLoader()
                      : loader);
    try
      {
        clazz = Class.forName(fullclassname, false, cl);
      }
    catch (ClassNotFoundException cnfe)
      {
        throw new RMICException
          ("Class " + fullclassname + " not found in classpath", cnfe);
      }

    if (! Remote.class.isAssignableFrom(clazz))
      {
        throw new RMICException
          ("Class " + clazz.getName()
           + " does not implement a remote interface.");
      }
  }

  private static Type[] typeArray(Class[] cls)
  {
    Type[] t = new Type[cls.length];
    for (int i = 0; i < cls.length; i++)
      {
        t[i] = Type.getType(cls[i]);
      }

    return t;
  }

  private static String[] internalNameArray(Type[] t)
  {
    String[] s = new String[t.length];
    for (int i = 0; i < t.length; i++)
      {
        s[i] = t[i].getInternalName();
      }

    return s;
  }

  private static String[] internalNameArray(Class[] c)
  {
    return internalNameArray(typeArray(c));
  }

  private static final String forName = "class$";

  private static Object param(Method m, int argIndex)
  {
    List l = new ArrayList();
    l.add(m);
    l.add(new Integer(argIndex));
    return l;
  }

  private static void generateClassForNamer(ClassVisitor cls)
  {
    MethodVisitor cv =
      cls.visitMethod
      (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, forName,
       Type.getMethodDescriptor
       (Type.getType(Class.class), new Type[] { Type.getType(String.class) }),
       null, null);

    Label start = new Label();
    cv.visitLabel(start);
    cv.visitVarInsn(Opcodes.ALOAD, 0);
    cv.visitMethodInsn
      (Opcodes.INVOKESTATIC,
       Type.getInternalName(Class.class),
       "forName",
       Type.getMethodDescriptor
       (Type.getType(Class.class), new Type[] { Type.getType(String.class) }));
    cv.visitInsn(Opcodes.ARETURN);

    Label handler = new Label();
    cv.visitLabel(handler);
    cv.visitVarInsn(Opcodes.ASTORE, 1);
    cv.visitTypeInsn(Opcodes.NEW, typeArg(NoClassDefFoundError.class));
    cv.visitInsn(Opcodes.DUP);
    cv.visitVarInsn(Opcodes.ALOAD, 1);
    cv.visitMethodInsn
      (Opcodes.INVOKEVIRTUAL,
       Type.getInternalName(ClassNotFoundException.class),
       "getMessage",
       Type.getMethodDescriptor(Type.getType(String.class), new Type[] {}));
    cv.visitMethodInsn
      (Opcodes.INVOKESPECIAL,
       Type.getInternalName(NoClassDefFoundError.class),
       "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] { Type.getType(String.class) }));
    cv.visitInsn(Opcodes.ATHROW);
    cv.visitTryCatchBlock
      (start, handler, handler,
       Type.getInternalName(ClassNotFoundException.class));
    cv.visitMaxs(-1, -1);
  }

  private void generateClassConstant(MethodVisitor cv, Class cls) {
    if (cls.isPrimitive())
      {
        Class boxCls;
        if (cls.equals(Boolean.TYPE))
          boxCls = Boolean.class;
        else if (cls.equals(Character.TYPE))
          boxCls = Character.class;
        else if (cls.equals(Byte.TYPE))
          boxCls = Byte.class;
        else if (cls.equals(Short.TYPE))
          boxCls = Short.class;
        else if (cls.equals(Integer.TYPE))
          boxCls = Integer.class;
        else if (cls.equals(Long.TYPE))
          boxCls = Long.class;
        else if (cls.equals(Float.TYPE))
          boxCls = Float.class;
        else if (cls.equals(Double.TYPE))
          boxCls = Double.class;
        else if (cls.equals(Void.TYPE))
          boxCls = Void.class;
        else
          throw new IllegalArgumentException("unknown primitive type " + cls);

        cv.visitFieldInsn
          (Opcodes.GETSTATIC, Type.getInternalName(boxCls), "TYPE",
           Type.getDescriptor(Class.class));
        return;
      }
    cv.visitLdcInsn(cls.getName());
    cv.visitMethodInsn
      (Opcodes.INVOKESTATIC, classInternalName, forName,
       Type.getMethodDescriptor
       (Type.getType(Class.class),
        new Type[] { Type.getType(String.class) }));
  }

  private void generateClassArray(MethodVisitor code, Class[] classes)
  {
    code.visitLdcInsn(new Integer(classes.length));
    code.visitTypeInsn(Opcodes.ANEWARRAY, typeArg(Class.class));
    for (int i = 0; i < classes.length; i++)
      {
        code.visitInsn(Opcodes.DUP);
        code.visitLdcInsn(new Integer(i));
        generateClassConstant(code, classes[i]);
        code.visitInsn(Opcodes.AASTORE);
      }
  }

  private void fillOperationArray(MethodVisitor clinit)
  {
    // Operations array
    clinit.visitLdcInsn(new Integer(remotemethods.length));
    clinit.visitTypeInsn(Opcodes.ANEWARRAY, typeArg(Operation.class));
    clinit.visitFieldInsn
      (Opcodes.PUTSTATIC, classInternalName, "operations",
       Type.getDescriptor(Operation[].class));

    for (int i = 0; i < remotemethods.length; i++)
      {
        Method m = remotemethods[i].meth;

        StringBuffer desc = new StringBuffer();
        desc.append(getPrettyName(m.getReturnType()) + " ");
        desc.append(m.getName() + "(");

        // signature
        Class[] sig = m.getParameterTypes();
        for (int j = 0; j < sig.length; j++)
          {
            desc.append(getPrettyName(sig[j]));
            if (j + 1 < sig.length)
                desc.append(", ");
          }

        // push operations array
        clinit.visitFieldInsn
          (Opcodes.GETSTATIC, classInternalName, "operations",
           Type.getDescriptor(Operation[].class));

        // push array index
        clinit.visitLdcInsn(new Integer(i));

        // instantiate operation and leave a copy on the stack
        clinit.visitTypeInsn(Opcodes.NEW, typeArg(Operation.class));
        clinit.visitInsn(Opcodes.DUP);
        clinit.visitLdcInsn(desc.toString());
        clinit.visitMethodInsn
          (Opcodes.INVOKESPECIAL,
           Type.getInternalName(Operation.class),
           "<init>",
           Type.getMethodDescriptor
           (Type.VOID_TYPE, new Type[] { Type.getType(String.class) }));

        // store in operations array
        clinit.visitInsn(Opcodes.AASTORE);
      }
  }

  private void generateStaticMethodObjs(MethodVisitor clinit)
  {
    for (int i = 0; i < remotemethods.length; i++)
      {
        Method m = remotemethods[i].meth;

        /*
         * $method_<i>m.getName()</i>_<i>i</i> =
         *   <i>m.getDeclaringClass()</i>.class.getMethod
         *     (m.getName(), m.getParameterType())
         */
        String methodVar = "$method_" + m.getName() + "_" + i;
        generateClassConstant(clinit, m.getDeclaringClass());
        clinit.visitLdcInsn(m.getName());
        generateClassArray(clinit, m.getParameterTypes());
        clinit.visitMethodInsn
          (Opcodes.INVOKEVIRTUAL,
           Type.getInternalName(Class.class),
           "getMethod",
           Type.getMethodDescriptor
           (Type.getType(Method.class),
            new Type[] { Type.getType(String.class),
                         Type.getType(Class[].class) }));

        clinit.visitFieldInsn
          (Opcodes.PUTSTATIC, classInternalName, methodVar,
           Type.getDescriptor(Method.class));
      }
  }

  private void generateStub()
    throws IOException
  {
    stubname = fullclassname + "_Stub";
    String stubclassname = classname + "_Stub";
    File file = new File((destination == null ? "." : destination)
                         + File.separator
                         + stubname.replace('.', File.separatorChar)
                         + ".class");

    if (verbose)
      System.out.println("[Generating class " + stubname + "]");

    final ClassWriter stub = new ClassWriter(true);
    classInternalName = stubname.replace('.', '/');
    final String superInternalName =
      Type.getType(RemoteStub.class).getInternalName();

    String[] remoteInternalNames =
      internalNameArray((Class[]) mRemoteInterfaces.toArray(new Class[] {}));
    stub.visit
      (Opcodes.V1_2, Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL, classInternalName,
       null, superInternalName, remoteInternalNames);

    if (need12Stubs)
      {
        stub.visitField
          (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL, "serialVersionUID",
           Type.LONG_TYPE.getDescriptor(), null, new Long(2L));
      }

    if (need11Stubs)
      {
        stub.visitField
          (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
           "interfaceHash", Type.LONG_TYPE.getDescriptor(), null,
           new Long(RMIHashes.getInterfaceHash(clazz)));

        if (need12Stubs)
          {
            stub.visitField
              (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC, "useNewInvoke",
               Type.BOOLEAN_TYPE.getDescriptor(), null, null);
          }

        stub.visitField
          (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
           "operations", Type.getDescriptor(Operation[].class), null, null);
      }

    // Set of method references.
    if (need12Stubs)
      {
        for (int i = 0; i < remotemethods.length; i++)
          {
            Method m = remotemethods[i].meth;
            String slotName = "$method_" + m.getName() + "_" + i;
            stub.visitField
              (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC, slotName,
               Type.getDescriptor(Method.class), null, null);
          }
      }

    MethodVisitor clinit = stub.visitMethod
      (Opcodes.ACC_STATIC, "<clinit>",
       Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}), null, null);

    if (need11Stubs)
      {
        fillOperationArray(clinit);
        if (! need12Stubs)
          clinit.visitInsn(Opcodes.RETURN);
      }

    if (need12Stubs)
      {
        // begin of try
        Label begin = new Label();

        // beginning of catch
        Label handler = new Label();
        clinit.visitLabel(begin);

        // Initialize the methods references.
        if (need11Stubs)
          {
            /*
             * RemoteRef.class.getMethod("invoke", new Class[] {
             *   Remote.class, Method.class, Object[].class, long.class })
             */
            generateClassConstant(clinit, RemoteRef.class);
            clinit.visitLdcInsn("invoke");
            generateClassArray
              (clinit, new Class[] { Remote.class, Method.class,
                                     Object[].class, long.class });
            clinit.visitMethodInsn
              (Opcodes.INVOKEVIRTUAL,
               Type.getInternalName(Class.class),
               "getMethod",
               Type.getMethodDescriptor
               (Type.getType(Method.class),
                new Type[] { Type.getType(String.class),
                             Type.getType(Class[].class) }));

            // useNewInvoke = true
            clinit.visitInsn(Opcodes.ICONST_1);
            clinit.visitFieldInsn
              (Opcodes.PUTSTATIC, classInternalName, "useNewInvoke",
               Type.BOOLEAN_TYPE.getDescriptor());
          }

        generateStaticMethodObjs(clinit);

        // jump past handler
        clinit.visitInsn(Opcodes.RETURN);
        clinit.visitLabel(handler);
        if (need11Stubs)
          {
            // useNewInvoke = false
            clinit.visitInsn(Opcodes.ICONST_0);
            clinit.visitFieldInsn
              (Opcodes.PUTSTATIC, classInternalName, "useNewInvoke",
               Type.BOOLEAN_TYPE.getDescriptor());
            clinit.visitInsn(Opcodes.RETURN);
          }
        else
          {
            // throw NoSuchMethodError
            clinit.visitTypeInsn(Opcodes.NEW, typeArg(NoSuchMethodError.class));
            clinit.visitInsn(Opcodes.DUP);
            clinit.visitLdcInsn("stub class initialization failed");
            clinit.visitMethodInsn
              (Opcodes.INVOKESPECIAL,
               Type.getInternalName(NoSuchMethodError.class),
               "<init>",
               Type.getMethodDescriptor
               (Type.VOID_TYPE,
                new Type[] { Type.getType(String.class) }));
            clinit.visitInsn(Opcodes.ATHROW);
          }

        clinit.visitTryCatchBlock
          (begin, handler, handler,
           Type.getInternalName(NoSuchMethodException.class));

      }

    clinit.visitMaxs(-1, -1);

    generateClassForNamer(stub);

    // Constructors
    if (need11Stubs)
      {
        // no arg public constructor
        MethodVisitor code = stub.visitMethod
          (Opcodes.ACC_PUBLIC, "<init>",
           Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}),
           null, null);
        code.visitVarInsn(Opcodes.ALOAD, 0);
        code.visitMethodInsn
          (Opcodes.INVOKESPECIAL, superInternalName, "<init>",
           Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
        code.visitInsn(Opcodes.RETURN);

        code.visitMaxs(-1, -1);
      }

    // public RemoteRef constructor
    MethodVisitor constructor = stub.visitMethod
      (Opcodes.ACC_PUBLIC, "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] {Type.getType(RemoteRef.class)}),
       null, null);
    constructor.visitVarInsn(Opcodes.ALOAD, 0);
    constructor.visitVarInsn(Opcodes.ALOAD, 1);
    constructor.visitMethodInsn
      (Opcodes.INVOKESPECIAL, superInternalName, "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] {Type.getType(RemoteRef.class)}));
    constructor.visitInsn(Opcodes.RETURN);
    constructor.visitMaxs(-1, -1);

    // Method implementations
    for (int i = 0; i < remotemethods.length; i++)
      {
        Method m = remotemethods[i].meth;
        Class[] sig = m.getParameterTypes();
        Class returntype = m.getReturnType();
        Class[] except = sortExceptions
          ((Class[]) remotemethods[i].exceptions.toArray(new Class[0]));

        MethodVisitor code = stub.visitMethod
          (Opcodes.ACC_PUBLIC,
           m.getName(),
           Type.getMethodDescriptor(Type.getType(returntype), typeArray(sig)),
           null,
           internalNameArray(typeArray(except)));

        final Variables var = new Variables();

        // this and parameters are the declared vars
        var.declare("this");
        for (int j = 0; j < sig.length; j++)
          var.declare(param(m, j), size(sig[j]));

        Label methodTryBegin = new Label();
        code.visitLabel(methodTryBegin);

        if (need12Stubs)
          {
            Label oldInvoke = new Label();
            if (need11Stubs)
              {
                // if not useNewInvoke jump to old invoke
                code.visitFieldInsn
                  (Opcodes.GETSTATIC, classInternalName, "useNewInvoke",
                   Type.getDescriptor(boolean.class));
                code.visitJumpInsn(Opcodes.IFEQ, oldInvoke);
              }

            // this.ref
            code.visitVarInsn(Opcodes.ALOAD, var.get("this"));
            code.visitFieldInsn
              (Opcodes.GETFIELD, Type.getInternalName(RemoteObject.class),
               "ref", Type.getDescriptor(RemoteRef.class));

            // "this" is first arg to invoke
            code.visitVarInsn(Opcodes.ALOAD, var.get("this"));

            // method object is second arg to invoke
            String methName = "$method_" + m.getName() + "_" + i;
            code.visitFieldInsn
              (Opcodes.GETSTATIC, classInternalName, methName,
               Type.getDescriptor(Method.class));

            // args to remote method are third arg to invoke
            if (sig.length == 0)
              code.visitInsn(Opcodes.ACONST_NULL);
            else
              {
                // create arg Object[] (with boxed primitives) and push it
                code.visitLdcInsn(new Integer(sig.length));
                code.visitTypeInsn(Opcodes.ANEWARRAY, typeArg(Object.class));

                var.allocate("argArray");
                code.visitVarInsn(Opcodes.ASTORE, var.get("argArray"));

                for (int j = 0; j < sig.length; j++)
                  {
                    int size = size(sig[j]);
                    int insn = loadOpcode(sig[j]);
                    Class box = sig[j].isPrimitive() ? box(sig[j]) : null;

                    code.visitVarInsn(Opcodes.ALOAD, var.get("argArray"));
                    code.visitLdcInsn(new Integer(j));

                    // put argument on stack
                    if (box != null)
                      {
                        code.visitTypeInsn(Opcodes.NEW, typeArg(box));
                        code.visitInsn(Opcodes.DUP);
                        code.visitVarInsn(insn, var.get(param(m, j)));
                        code.visitMethodInsn
                          (Opcodes.INVOKESPECIAL,
                           Type.getInternalName(box),
                           "<init>",
                           Type.getMethodDescriptor
                           (Type.VOID_TYPE,
                            new Type[] { Type.getType(sig[j]) }));
                      }
                    else
                      code.visitVarInsn(insn, var.get(param(m, j)));

                    code.visitInsn(Opcodes.AASTORE);
                  }

                code.visitVarInsn(Opcodes.ALOAD, var.deallocate("argArray"));
              }

            // push remote operation opcode
            code.visitLdcInsn(new Long(remotemethods[i].hash));
            code.visitMethodInsn
              (Opcodes.INVOKEINTERFACE,
               Type.getInternalName(RemoteRef.class),
               "invoke",
               Type.getMethodDescriptor
               (Type.getType(Object.class),
                new Type[] { Type.getType(Remote.class),
                             Type.getType(Method.class),
                             Type.getType(Object[].class),
                             Type.LONG_TYPE }));

            if (! returntype.equals(Void.TYPE))
              {
                int retcode = returnOpcode(returntype);
                Class boxCls =
                  returntype.isPrimitive() ? box(returntype) : null;
                code.visitTypeInsn
                  (Opcodes.CHECKCAST, typeArg(boxCls == null ? returntype : boxCls));
                if (returntype.isPrimitive())
                  {
                    // unbox
                    code.visitMethodInsn
                      (Opcodes.INVOKEVIRTUAL,
                       Type.getType(boxCls).getInternalName(),
                       unboxMethod(returntype),
                       Type.getMethodDescriptor
                       (Type.getType(returntype), new Type[] {}));
                  }

                code.visitInsn(retcode);
              }
            else
              code.visitInsn(Opcodes.RETURN);


            if (need11Stubs)
              code.visitLabel(oldInvoke);
          }

        if (need11Stubs)
          {

            // this.ref.newCall(this, operations, index, interfaceHash)
            code.visitVarInsn(Opcodes.ALOAD, var.get("this"));
            code.visitFieldInsn
              (Opcodes.GETFIELD,
               Type.getInternalName(RemoteObject.class),
               "ref",
               Type.getDescriptor(RemoteRef.class));

            // "this" is first arg to newCall
            code.visitVarInsn(Opcodes.ALOAD, var.get("this"));

            // operations is second arg to newCall
            code.visitFieldInsn
              (Opcodes.GETSTATIC, classInternalName, "operations",
               Type.getDescriptor(Operation[].class));

            // method index is third arg
            code.visitLdcInsn(new Integer(i));

            // interface hash is fourth arg
            code.visitFieldInsn
              (Opcodes.GETSTATIC, classInternalName, "interfaceHash",
               Type.LONG_TYPE.getDescriptor());

            code.visitMethodInsn
              (Opcodes.INVOKEINTERFACE,
               Type.getInternalName(RemoteRef.class),
               "newCall",
               Type.getMethodDescriptor
               (Type.getType(RemoteCall.class),
                new Type[] { Type.getType(RemoteObject.class),
                             Type.getType(Operation[].class),
                             Type.INT_TYPE,
                             Type.LONG_TYPE }));

            // store call object on stack and leave copy on stack
            var.allocate("call");
            code.visitInsn(Opcodes.DUP);
            code.visitVarInsn(Opcodes.ASTORE, var.get("call"));

            Label beginArgumentTryBlock = new Label();
            code.visitLabel(beginArgumentTryBlock);

            // ObjectOutput out = call.getOutputStream();
            code.visitMethodInsn
              (Opcodes.INVOKEINTERFACE,
               Type.getInternalName(RemoteCall.class),
               "getOutputStream",
               Type.getMethodDescriptor
               (Type.getType(ObjectOutput.class), new Type[] {}));

            for (int j = 0; j < sig.length; j++)
              {
                // dup the ObjectOutput
                code.visitInsn(Opcodes.DUP);

                // get j'th arg to remote method
                code.visitVarInsn(loadOpcode(sig[j]), var.get(param(m, j)));

                Class argCls =
                  sig[j].isPrimitive() ? sig[j] : Object.class;

                // out.writeFoo
                code.visitMethodInsn
                  (Opcodes.INVOKEINTERFACE,
                   Type.getInternalName(ObjectOutput.class),
                   writeMethod(sig[j]),
                   Type.getMethodDescriptor
                   (Type.VOID_TYPE,
                    new Type[] { Type.getType(argCls) }));
              }

            // pop ObjectOutput
            code.visitInsn(Opcodes.POP);

            Label iohandler = new Label();
            Label endArgumentTryBlock = new Label();
            code.visitJumpInsn(Opcodes.GOTO, endArgumentTryBlock);
            code.visitLabel(iohandler);

            // throw new MarshalException(msg, ioexception);
            code.visitVarInsn(Opcodes.ASTORE, var.allocate("exception"));
            code.visitTypeInsn(Opcodes.NEW, typeArg(MarshalException.class));
            code.visitInsn(Opcodes.DUP);
            code.visitLdcInsn("error marshalling arguments");
            code.visitVarInsn(Opcodes.ALOAD, var.deallocate("exception"));
            code.visitMethodInsn
              (Opcodes.INVOKESPECIAL,
               Type.getInternalName(MarshalException.class),
               "<init>",
               Type.getMethodDescriptor
               (Type.VOID_TYPE,
                new Type[] { Type.getType(String.class),
                             Type.getType(Exception.class) }));
            code.visitInsn(Opcodes.ATHROW);

            code.visitLabel(endArgumentTryBlock);
            code.visitTryCatchBlock
              (beginArgumentTryBlock, iohandler, iohandler,
               Type.getInternalName(IOException.class));

            // this.ref.invoke(call)
            code.visitVarInsn(Opcodes.ALOAD, var.get("this"));
            code.visitFieldInsn
              (Opcodes.GETFIELD, Type.getInternalName(RemoteObject.class),
               "ref", Type.getDescriptor(RemoteRef.class));
            code.visitVarInsn(Opcodes.ALOAD, var.get("call"));
            code.visitMethodInsn
              (Opcodes.INVOKEINTERFACE,
               Type.getInternalName(RemoteRef.class),
               "invoke",
               Type.getMethodDescriptor
               (Type.VOID_TYPE,
                new Type[] { Type.getType(RemoteCall.class) }));

            // handle return value
            boolean needcastcheck = false;

            Label beginReturnTryCatch = new Label();
            code.visitLabel(beginReturnTryCatch);

            int returncode = returnOpcode(returntype);

            if (! returntype.equals(Void.TYPE))
              {
                // call.getInputStream()
                code.visitVarInsn(Opcodes.ALOAD, var.get("call"));
                code.visitMethodInsn
                  (Opcodes.INVOKEINTERFACE,
                   Type.getInternalName(RemoteCall.class),
                   "getInputStream",
                   Type.getMethodDescriptor
                   (Type.getType(ObjectInput.class), new Type[] {}));

                Class readCls =
                  returntype.isPrimitive() ? returntype : Object.class;
                code.visitMethodInsn
                  (Opcodes.INVOKEINTERFACE,
                   Type.getInternalName(ObjectInput.class),
                   readMethod(returntype),
                   Type.getMethodDescriptor
                   (Type.getType(readCls), new Type[] {}));

                boolean castresult = false;

                if (! returntype.isPrimitive())
                  {
                    if (! returntype.equals(Object.class))
                      castresult = true;
                    else
                      needcastcheck = true;
                  }

                if (castresult)
                  code.visitTypeInsn(Opcodes.CHECKCAST, typeArg(returntype));

                // leave result on stack for return
              }

            // this.ref.done(call)
            code.visitVarInsn(Opcodes.ALOAD, var.get("this"));
            code.visitFieldInsn
              (Opcodes.GETFIELD,
               Type.getInternalName(RemoteObject.class),
               "ref",
               Type.getDescriptor(RemoteRef.class));
            code.visitVarInsn(Opcodes.ALOAD, var.deallocate("call"));
            code.visitMethodInsn
              (Opcodes.INVOKEINTERFACE,
               Type.getInternalName(RemoteRef.class),
               "done",
               Type.getMethodDescriptor
               (Type.VOID_TYPE,
                new Type[] { Type.getType(RemoteCall.class) }));

            // return; or return result;
            code.visitInsn(returncode);

            // exception handler
            Label handler = new Label();
            code.visitLabel(handler);
            code.visitVarInsn(Opcodes.ASTORE, var.allocate("exception"));

            // throw new UnmarshalException(msg, e)
            code.visitTypeInsn(Opcodes.NEW, typeArg(UnmarshalException.class));
            code.visitInsn(Opcodes.DUP);
            code.visitLdcInsn("error unmarshalling return");
            code.visitVarInsn(Opcodes.ALOAD, var.deallocate("exception"));
            code.visitMethodInsn
              (Opcodes.INVOKESPECIAL,
               Type.getInternalName(UnmarshalException.class),
               "<init>",
               Type.getMethodDescriptor
               (Type.VOID_TYPE,
                new Type[] { Type.getType(String.class),
                             Type.getType(Exception.class) }));
            code.visitInsn(Opcodes.ATHROW);

            Label endReturnTryCatch = new Label();

            // catch IOException
            code.visitTryCatchBlock
              (beginReturnTryCatch, handler, handler,
               Type.getInternalName(IOException.class));

            if (needcastcheck)
              {
                // catch ClassNotFoundException
                code.visitTryCatchBlock
                  (beginReturnTryCatch, handler, handler,
                   Type.getInternalName(ClassNotFoundException.class));
              }
          }

        Label rethrowHandler = new Label();
        code.visitLabel(rethrowHandler);
        // rethrow declared exceptions
        code.visitInsn(Opcodes.ATHROW);

        boolean needgeneral = true;
        for (int j = 0; j < except.length; j++)
          {
            if (except[j] == Exception.class)
              needgeneral = false;
          }

        for (int j = 0; j < except.length; j++)
          {
            code.visitTryCatchBlock
              (methodTryBegin, rethrowHandler, rethrowHandler,
               Type.getInternalName(except[j]));
          }

        if (needgeneral)
          {
            // rethrow unchecked exceptions
            code.visitTryCatchBlock
              (methodTryBegin, rethrowHandler, rethrowHandler,
               Type.getInternalName(RuntimeException.class));

            Label generalHandler = new Label();
            code.visitLabel(generalHandler);
            String msg = "undeclared checked exception";

            // throw new java.rmi.UnexpectedException(msg, e)
            code.visitVarInsn(Opcodes.ASTORE, var.allocate("exception"));
            code.visitTypeInsn(Opcodes.NEW, typeArg(UnexpectedException.class));
            code.visitInsn(Opcodes.DUP);
            code.visitLdcInsn(msg);
            code.visitVarInsn(Opcodes.ALOAD, var.deallocate("exception"));
            code.visitMethodInsn
              (Opcodes.INVOKESPECIAL,
               Type.getInternalName(UnexpectedException.class),
               "<init>",
               Type.getMethodDescriptor
               (Type.VOID_TYPE,
                new Type [] { Type.getType(String.class),
                              Type.getType(Exception.class) }));
            code.visitInsn(Opcodes.ATHROW);

            code.visitTryCatchBlock
              (methodTryBegin, rethrowHandler, generalHandler,
               Type.getInternalName(Exception.class));
          }

        code.visitMaxs(-1, -1);
      }

    stub.visitEnd();
    byte[] classData = stub.toByteArray();
    if (!noWrite)
      {
        if (file.exists())
          file.delete();
        if (file.getParentFile() != null)
          file.getParentFile().mkdirs();
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(classData);
        fos.flush();
        fos.close();
      }
  }

  private void generateSkel() throws IOException
  {
    skelname = fullclassname + "_Skel";
    String skelclassname = classname + "_Skel";
    File file = new File(destination == null ? "" : destination
                         + File.separator
                         + skelname.replace('.', File.separatorChar)
                         + ".class");
    if (verbose)
      System.out.println("[Generating class " + skelname + "]");

    final ClassWriter skel = new ClassWriter(true);
    classInternalName = skelname.replace('.', '/');
    skel.visit
      (Opcodes.V1_1, Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL,
       classInternalName, Type.getInternalName(Object.class), null,
       new String[] { Type.getType(Skeleton.class).getInternalName() });

    skel.visitField
      (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL, "interfaceHash",
       Type.LONG_TYPE.getDescriptor(), null,
       new Long(RMIHashes.getInterfaceHash(clazz)));

    skel.visitField
      (Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL, "operations",
       Type.getDescriptor(Operation[].class), null, null);

    MethodVisitor clinit = skel.visitMethod
      (Opcodes.ACC_STATIC, "<clinit>",
       Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}), null, null);

    fillOperationArray(clinit);
    clinit.visitInsn(Opcodes.RETURN);

    clinit.visitMaxs(-1, -1);

    // no arg public constructor
    MethodVisitor init = skel.visitMethod
      (Opcodes.ACC_PUBLIC, "<init>",
       Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}), null, null);
    init.visitVarInsn(Opcodes.ALOAD, 0);
    init.visitMethodInsn
      (Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "<init>",
       Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
    init.visitInsn(Opcodes.RETURN);
    init.visitMaxs(-1, -1);

    /*
     * public Operation[] getOperations()
     * returns a clone of the operations array
     */
    MethodVisitor getOp = skel.visitMethod
      (Opcodes.ACC_PUBLIC, "getOperations",
       Type.getMethodDescriptor
       (Type.getType(Operation[].class), new Type[] {}),
       null, null);
    getOp.visitFieldInsn
      (Opcodes.GETSTATIC, classInternalName, "operations",
       Type.getDescriptor(Operation[].class));
    getOp.visitMethodInsn
      (Opcodes.INVOKEVIRTUAL, Type.getInternalName(Object.class),
       "clone", Type.getMethodDescriptor(Type.getType(Object.class),
                                         new Type[] {}));
    getOp.visitTypeInsn(Opcodes.CHECKCAST, typeArg(Operation[].class));
    getOp.visitInsn(Opcodes.ARETURN);
    getOp.visitMaxs(-1, -1);

    // public void dispatch(Remote, RemoteCall, int opnum, long hash)
    MethodVisitor dispatch = skel.visitMethod
      (Opcodes.ACC_PUBLIC,
       "dispatch",
       Type.getMethodDescriptor
       (Type.VOID_TYPE,
        new Type[] { Type.getType(Remote.class),
                     Type.getType(RemoteCall.class),
                     Type.INT_TYPE, Type.LONG_TYPE }), null,
       new String[] { Type.getInternalName(Exception.class) });

    Variables var = new Variables();
    var.declare("this");
    var.declare("remoteobj");
    var.declare("remotecall");
    var.declare("opnum");
    var.declareWide("hash");

    /*
     * if opnum >= 0
     * XXX it is unclear why there is handling of negative opnums
     */
    dispatch.visitVarInsn(Opcodes.ILOAD, var.get("opnum"));
    Label nonNegativeOpnum = new Label();
    Label opnumSet = new Label();
    dispatch.visitJumpInsn(Opcodes.IFGE, nonNegativeOpnum);

    for (int i = 0; i < remotemethods.length; i++)
      {
        // assign opnum if hash matches supplied hash
        dispatch.visitVarInsn(Opcodes.LLOAD, var.get("hash"));
        dispatch.visitLdcInsn(new Long(remotemethods[i].hash));
        Label notIt = new Label();
        dispatch.visitInsn(Opcodes.LCMP);
        dispatch.visitJumpInsn(Opcodes.IFNE, notIt);

        // opnum = <opnum>
        dispatch.visitLdcInsn(new Integer(i));
        dispatch.visitVarInsn(Opcodes.ISTORE, var.get("opnum"));
        dispatch.visitJumpInsn(Opcodes.GOTO, opnumSet);
        dispatch.visitLabel(notIt);
      }

    // throw new SkeletonMismatchException
    Label mismatch = new Label();
    dispatch.visitJumpInsn(Opcodes.GOTO, mismatch);

    dispatch.visitLabel(nonNegativeOpnum);

    // if opnum is already set, check that the hash matches the interface
    dispatch.visitVarInsn(Opcodes.LLOAD, var.get("hash"));
    dispatch.visitFieldInsn
      (Opcodes.GETSTATIC, classInternalName,
       "interfaceHash", Type.LONG_TYPE.getDescriptor());
    dispatch.visitInsn(Opcodes.LCMP);
    dispatch.visitJumpInsn(Opcodes.IFEQ, opnumSet);

    dispatch.visitLabel(mismatch);
    dispatch.visitTypeInsn
      (Opcodes.NEW, typeArg(SkeletonMismatchException.class));
    dispatch.visitInsn(Opcodes.DUP);
    dispatch.visitLdcInsn("interface hash mismatch");
    dispatch.visitMethodInsn
      (Opcodes.INVOKESPECIAL,
       Type.getInternalName(SkeletonMismatchException.class),
       "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] { Type.getType(String.class) }));
    dispatch.visitInsn(Opcodes.ATHROW);

    // opnum has been set
    dispatch.visitLabel(opnumSet);

    dispatch.visitVarInsn(Opcodes.ALOAD, var.get("remoteobj"));
    dispatch.visitTypeInsn(Opcodes.CHECKCAST, typeArg(clazz));
    dispatch.visitVarInsn(Opcodes.ASTORE, var.get("remoteobj"));

    Label deflt = new Label();
    Label[] methLabels = new Label[remotemethods.length];
    for (int i = 0; i < methLabels.length; i++)
      methLabels[i] = new Label();

    // switch on opnum
    dispatch.visitVarInsn(Opcodes.ILOAD, var.get("opnum"));
    dispatch.visitTableSwitchInsn
      (0, remotemethods.length - 1, deflt, methLabels);

    // Method dispatch
    for (int i = 0; i < remotemethods.length; i++)
      {
        dispatch.visitLabel(methLabels[i]);
        Method m = remotemethods[i].meth;
        generateMethodSkel(dispatch, m, var);
      }

    dispatch.visitLabel(deflt);
    dispatch.visitTypeInsn(Opcodes.NEW, typeArg(UnmarshalException.class));
    dispatch.visitInsn(Opcodes.DUP);
    dispatch.visitLdcInsn("invalid method number");
    dispatch.visitMethodInsn
      (Opcodes.INVOKESPECIAL,
       Type.getInternalName(UnmarshalException.class),
       "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] { Type.getType(String.class) }));
    dispatch.visitInsn(Opcodes.ATHROW);

    dispatch.visitMaxs(-1, -1);

    skel.visitEnd();
    byte[] classData = skel.toByteArray();
    if (!noWrite)
      {
        if (file.exists())
          file.delete();
        if (file.getParentFile() != null)
          file.getParentFile().mkdirs();
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(classData);
        fos.flush();
        fos.close();
      }
  }

  private void generateMethodSkel(MethodVisitor cv, Method m, Variables var)
  {
    Class[] sig = m.getParameterTypes();

    Label readArgs = new Label();
    cv.visitLabel(readArgs);

    boolean needcastcheck = false;

    // ObjectInput in = call.getInputStream();
    cv.visitVarInsn(Opcodes.ALOAD, var.get("remotecall"));
    cv.visitMethodInsn
      (Opcodes.INVOKEINTERFACE,
       Type.getInternalName(RemoteCall.class), "getInputStream",
       Type.getMethodDescriptor
       (Type.getType(ObjectInput.class), new Type[] {}));
    cv.visitVarInsn(Opcodes.ASTORE, var.allocate("objectinput"));

    for (int i = 0; i < sig.length; i++)
      {
        // dup input stream
        cv.visitVarInsn(Opcodes.ALOAD, var.get("objectinput"));

        Class readCls = sig[i].isPrimitive() ? sig[i] : Object.class;

        // in.readFoo()
        cv.visitMethodInsn
          (Opcodes.INVOKEINTERFACE,
           Type.getInternalName(ObjectInput.class),
           readMethod(sig[i]),
           Type.getMethodDescriptor
           (Type.getType(readCls), new Type [] {}));

        if (! sig[i].isPrimitive() && ! sig[i].equals(Object.class))
          {
            needcastcheck = true;
            cv.visitTypeInsn(Opcodes.CHECKCAST, typeArg(sig[i]));
          }

        // store arg in variable
        cv.visitVarInsn
          (storeOpcode(sig[i]), var.allocate(param(m, i), size(sig[i])));
      }

    var.deallocate("objectinput");

    Label doCall = new Label();
    Label closeInput = new Label();

    cv.visitJumpInsn(Opcodes.JSR, closeInput);
    cv.visitJumpInsn(Opcodes.GOTO, doCall);

    // throw new UnmarshalException
    Label handler = new Label();
    cv.visitLabel(handler);
    cv.visitVarInsn(Opcodes.ASTORE, var.allocate("exception"));
    cv.visitTypeInsn(Opcodes.NEW, typeArg(UnmarshalException.class));
    cv.visitInsn(Opcodes.DUP);
    cv.visitLdcInsn("error unmarshalling arguments");
    cv.visitVarInsn(Opcodes.ALOAD, var.deallocate("exception"));
    cv.visitMethodInsn
      (Opcodes.INVOKESPECIAL,
       Type.getInternalName(UnmarshalException.class),
       "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] { Type.getType(String.class),
                                     Type.getType(Exception.class) }));
    cv.visitVarInsn(Opcodes.ASTORE, var.allocate("toThrow"));
    cv.visitJumpInsn(Opcodes.JSR, closeInput);
    cv.visitVarInsn(Opcodes.ALOAD, var.get("toThrow"));
    cv.visitInsn(Opcodes.ATHROW);

    cv.visitTryCatchBlock
      (readArgs, handler, handler, Type.getInternalName(IOException.class));
    if (needcastcheck)
      {
        cv.visitTryCatchBlock
          (readArgs, handler, handler,
           Type.getInternalName(ClassCastException.class));
      }

    // finally block
    cv.visitLabel(closeInput);
    cv.visitVarInsn(Opcodes.ASTORE, var.allocate("retAddress"));
    cv.visitVarInsn(Opcodes.ALOAD, var.get("remotecall"));
    cv.visitMethodInsn
      (Opcodes.INVOKEINTERFACE,
       Type.getInternalName(RemoteCall.class),
       "releaseInputStream",
       Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
    cv.visitVarInsn(Opcodes.RET, var.deallocate("retAddress"));
    var.deallocate("toThrow");

    // do the call using args stored as variables
    cv.visitLabel(doCall);
    cv.visitVarInsn(Opcodes.ALOAD, var.get("remoteobj"));
    for (int i = 0; i < sig.length; i++)
      cv.visitVarInsn(loadOpcode(sig[i]), var.deallocate(param(m, i)));
    cv.visitMethodInsn
      (Opcodes.INVOKEVIRTUAL, Type.getInternalName(clazz), m.getName(),
       Type.getMethodDescriptor(m));

    Class returntype = m.getReturnType();
    if (! returntype.equals(Void.TYPE))
      {
        cv.visitVarInsn
          (storeOpcode(returntype), var.allocate("result", size(returntype)));
      }

    // write result to result stream
    Label writeResult = new Label();
    cv.visitLabel(writeResult);
    cv.visitVarInsn(Opcodes.ALOAD, var.get("remotecall"));
    cv.visitInsn(Opcodes.ICONST_1);
    cv.visitMethodInsn
      (Opcodes.INVOKEINTERFACE,
       Type.getInternalName(RemoteCall.class),
       "getResultStream",
       Type.getMethodDescriptor
       (Type.getType(ObjectOutput.class),
        new Type[] { Type.BOOLEAN_TYPE }));

    if (! returntype.equals(Void.TYPE))
      {
        // out.writeFoo(result)
        cv.visitVarInsn(loadOpcode(returntype), var.deallocate("result"));
        Class writeCls = returntype.isPrimitive() ? returntype : Object.class;
        cv.visitMethodInsn
          (Opcodes.INVOKEINTERFACE,
           Type.getInternalName(ObjectOutput.class),
           writeMethod(returntype),
           Type.getMethodDescriptor
           (Type.VOID_TYPE, new Type[] { Type.getType(writeCls) }));
      }

    cv.visitInsn(Opcodes.RETURN);

    // throw new MarshalException
    Label marshalHandler = new Label();
    cv.visitLabel(marshalHandler);
    cv.visitVarInsn(Opcodes.ASTORE, var.allocate("exception"));
    cv.visitTypeInsn(Opcodes.NEW, typeArg(MarshalException.class));
    cv.visitInsn(Opcodes.DUP);
    cv.visitLdcInsn("error marshalling return");
    cv.visitVarInsn(Opcodes.ALOAD, var.deallocate("exception"));
    cv.visitMethodInsn
      (Opcodes.INVOKESPECIAL,
       Type.getInternalName(MarshalException.class),
       "<init>",
       Type.getMethodDescriptor
       (Type.VOID_TYPE, new Type[] { Type.getType(String.class),
                                     Type.getType(Exception.class) }));
    cv.visitInsn(Opcodes.ATHROW);
    cv.visitTryCatchBlock
      (writeResult, marshalHandler, marshalHandler,
       Type.getInternalName(IOException.class));
  }

  private static String typeArg(Class cls)
  {
    if (cls.isArray())
      return Type.getDescriptor(cls);

    return Type.getInternalName(cls);
  }

  private static String readMethod(Class cls)
  {
    if (cls.equals(Void.TYPE))
      throw new IllegalArgumentException("can not read void");

    String method;
    if (cls.equals(Boolean.TYPE))
      method = "readBoolean";
    else if (cls.equals(Byte.TYPE))
      method = "readByte";
    else if (cls.equals(Character.TYPE))
      method = "readChar";
    else if (cls.equals(Short.TYPE))
      method = "readShort";
    else if (cls.equals(Integer.TYPE))
      method = "readInt";
    else if (cls.equals(Long.TYPE))
      method = "readLong";
    else if (cls.equals(Float.TYPE))
      method = "readFloat";
    else if (cls.equals(Double.TYPE))
      method = "readDouble";
    else
      method = "readObject";

    return method;
  }

  private static String writeMethod(Class cls)
  {
    if (cls.equals(Void.TYPE))
      throw new IllegalArgumentException("can not read void");

    String method;
    if (cls.equals(Boolean.TYPE))
      method = "writeBoolean";
    else if (cls.equals(Byte.TYPE))
      method = "writeByte";
    else if (cls.equals(Character.TYPE))
      method = "writeChar";
    else if (cls.equals(Short.TYPE))
      method = "writeShort";
    else if (cls.equals(Integer.TYPE))
      method = "writeInt";
    else if (cls.equals(Long.TYPE))
      method = "writeLong";
    else if (cls.equals(Float.TYPE))
      method = "writeFloat";
    else if (cls.equals(Double.TYPE))
      method = "writeDouble";
    else
      method = "writeObject";

    return method;
  }

  private static int returnOpcode(Class cls)
  {
    int returncode;
    if (cls.equals(Boolean.TYPE))
      returncode = Opcodes.IRETURN;
    else if (cls.equals(Byte.TYPE))
      returncode = Opcodes.IRETURN;
    else if (cls.equals(Character.TYPE))
      returncode = Opcodes.IRETURN;
    else if (cls.equals(Short.TYPE))
      returncode = Opcodes.IRETURN;
    else if (cls.equals(Integer.TYPE))
      returncode = Opcodes.IRETURN;
    else if (cls.equals(Long.TYPE))
      returncode = Opcodes.LRETURN;
    else if (cls.equals(Float.TYPE))
      returncode = Opcodes.FRETURN;
    else if (cls.equals(Double.TYPE))
      returncode = Opcodes.DRETURN;
    else if (cls.equals(Void.TYPE))
      returncode = Opcodes.RETURN;
    else
      returncode = Opcodes.ARETURN;

    return returncode;
  }

  private static int loadOpcode(Class cls)
  {
    if (cls.equals(Void.TYPE))
      throw new IllegalArgumentException("can not load void");

    int loadcode;
    if (cls.equals(Boolean.TYPE))
      loadcode = Opcodes.ILOAD;
    else if (cls.equals(Byte.TYPE))
      loadcode = Opcodes.ILOAD;
    else if (cls.equals(Character.TYPE))
      loadcode = Opcodes.ILOAD;
    else if (cls.equals(Short.TYPE))
      loadcode = Opcodes.ILOAD;
    else if (cls.equals(Integer.TYPE))
      loadcode = Opcodes.ILOAD;
    else if (cls.equals(Long.TYPE))
      loadcode = Opcodes.LLOAD;
    else if (cls.equals(Float.TYPE))
      loadcode = Opcodes.FLOAD;
    else if (cls.equals(Double.TYPE))
      loadcode = Opcodes.DLOAD;
    else
      loadcode = Opcodes.ALOAD;

    return loadcode;
  }

  private static int storeOpcode(Class cls)
  {
    if (cls.equals(Void.TYPE))
      throw new IllegalArgumentException("can not load void");

    int storecode;
    if (cls.equals(Boolean.TYPE))
      storecode = Opcodes.ISTORE;
    else if (cls.equals(Byte.TYPE))
      storecode = Opcodes.ISTORE;
    else if (cls.equals(Character.TYPE))
      storecode = Opcodes.ISTORE;
    else if (cls.equals(Short.TYPE))
      storecode = Opcodes.ISTORE;
    else if (cls.equals(Integer.TYPE))
      storecode = Opcodes.ISTORE;
    else if (cls.equals(Long.TYPE))
      storecode = Opcodes.LSTORE;
    else if (cls.equals(Float.TYPE))
      storecode = Opcodes.FSTORE;
    else if (cls.equals(Double.TYPE))
      storecode = Opcodes.DSTORE;
    else
      storecode = Opcodes.ASTORE;

    return storecode;
  }

  private static String unboxMethod(Class primitive)
  {
    if (! primitive.isPrimitive())
      throw new IllegalArgumentException("can not unbox nonprimitive");

    String method;
    if (primitive.equals(Boolean.TYPE))
      method = "booleanValue";
    else if (primitive.equals(Byte.TYPE))
      method = "byteValue";
    else if (primitive.equals(Character.TYPE))
      method = "charValue";
    else if (primitive.equals(Short.TYPE))
      method = "shortValue";
    else if (primitive.equals(Integer.TYPE))
      method = "intValue";
    else if (primitive.equals(Long.TYPE))
      method = "longValue";
    else if (primitive.equals(Float.TYPE))
      method = "floatValue";
    else if (primitive.equals(Double.TYPE))
      method = "doubleValue";
    else
      throw new IllegalStateException("unknown primitive class " + primitive);

    return method;
  }

  public static Class box(Class cls)
  {
    if (! cls.isPrimitive())
      throw new IllegalArgumentException("can only box primitive");

    Class box;
    if (cls.equals(Boolean.TYPE))
      box = Boolean.class;
    else if (cls.equals(Byte.TYPE))
      box = Byte.class;
    else if (cls.equals(Character.TYPE))
      box = Character.class;
    else if (cls.equals(Short.TYPE))
      box = Short.class;
    else if (cls.equals(Integer.TYPE))
      box = Integer.class;
    else if (cls.equals(Long.TYPE))
      box = Long.class;
    else if (cls.equals(Float.TYPE))
      box = Float.class;
    else if (cls.equals(Double.TYPE))
      box = Double.class;
    else
      throw new IllegalStateException("unknown primitive type " + cls);

    return box;
  }

  private static int size(Class cls) {
    if (cls.equals(Long.TYPE) || cls.equals(Double.TYPE))
      return 2;
    else
      return 1;
  }

  /**
   * Sort exceptions so the most general go last.
   */
  private Class[] sortExceptions(Class[] except)
  {
    for (int i = 0; i < except.length; i++)
      {
	for (int j = i + 1; j < except.length; j++)
	  {
	    if (except[i].isAssignableFrom(except[j]))
	      {
		Class tmp = except[i];
		except[i] = except[j];
		except[j] = tmp;
	      }
	  }
      }
    return (except);
  }

  public void setup(boolean keep, boolean need11Stubs, boolean need12Stubs,
                    boolean iiop, boolean poa, boolean debug, boolean warnings,
                    boolean noWrite, boolean verbose, boolean force, String classpath,
                    String bootclasspath, String extdirs, String outputDirectory)
  {
    this.keep = keep;
    this.need11Stubs = need11Stubs;
    this.need12Stubs = need12Stubs;
    this.verbose = verbose;
    this.noWrite = noWrite;

    // Set up classpath.
    this.classpath = classpath;
    StringTokenizer st =
      new StringTokenizer(classpath, File.pathSeparator);
    URL[] u = new URL[st.countTokens()];
    for (int i = 0; i < u.length; i++)
      {
        String path = st.nextToken();
        File f = new File(path);
        try
          {
            u[i] = f.toURL();
          }
        catch (java.net.MalformedURLException mue)
          {
            logError("malformed classpath component " + path);
            return;
          }
      }
    loader = new URLClassLoader(u);

    destination = outputDirectory;
  }

  private void findRemoteMethods()
    throws RMICException
  {
    List rmeths = new ArrayList();
    for (Class cur = clazz; cur != null; cur = cur.getSuperclass())
      {
        Class[] interfaces = cur.getInterfaces();
        for (int i = 0; i < interfaces.length; i++)
          {
            if (java.rmi.Remote.class.isAssignableFrom(interfaces[i]))
              {
                Class remoteInterface = interfaces[i];
                if (verbose)
                  System.out.println
                    ("[implements " + remoteInterface.getName() + "]");

                // check if the methods declare RemoteExceptions
                Method[] meths = remoteInterface.getMethods();
                for (int j = 0; j < meths.length; j++)
                  {
                    Method m = meths[j];
                    Class[] exs = m.getExceptionTypes();

                    boolean throwsRemote = false;
                    for (int k = 0; k < exs.length; k++)
                      {
                        if (exs[k].isAssignableFrom(RemoteException.class))
                          throwsRemote = true;
                      }

                    if (! throwsRemote)
                      {
                        throw new RMICException
                          ("Method " + m + " in interface " + remoteInterface
                           + " does not throw a RemoteException");
                      }

                    rmeths.add(m);
                  }

                mRemoteInterfaces.add(remoteInterface);
              }
          }
      }

    // intersect exceptions for doubly inherited methods
    boolean[] skip = new boolean[rmeths.size()];
    for (int i = 0; i < skip.length; i++)
      skip[i] = false;
    List methrefs = new ArrayList();
    for (int i = 0; i < rmeths.size(); i++)
      {
        if (skip[i]) continue;
        Method current = (Method) rmeths.get(i);
        MethodRef ref = new MethodRef(current);
        for (int j = i+1; j < rmeths.size(); j++)
          {
            Method other = (Method) rmeths.get(j);
            if (ref.isMatch(other))
              {
                ref.intersectExceptions(other);
                skip[j] = true;
              }
          }
        methrefs.add(ref);
      }

    // Convert into a MethodRef array and sort them
    remotemethods = (MethodRef[])
      methrefs.toArray(new MethodRef[methrefs.size()]);
    Arrays.sort(remotemethods);
  }

  /**
   * Prints an error to System.err and increases the error count.
   */
  private void logError(Exception theError)
  {
    logError(theError.getMessage());
    if (verbose)
      theError.printStackTrace(System.err);
  }

  /**
   * Prints an error to System.err and increases the error count.
   */
  private void logError(String theError)
  {
    errorCount++;
    System.err.println("error: " + theError);
  }

  private static String getPrettyName(Class cls)
  {
    StringBuffer str = new StringBuffer();
    for (int count = 0;; count++)
      {
	if (! cls.isArray())
	  {
	    str.append(cls.getName());
	    for (; count > 0; count--)
	      str.append("[]");
	    return (str.toString());
	  }
	cls = cls.getComponentType();
      }
  }

  private static class MethodRef
    implements Comparable
  {
    Method meth;
    long hash;
    List exceptions;
    private String sig;

    MethodRef(Method m) {
      meth = m;
      sig = Type.getMethodDescriptor(meth);
      hash = RMIHashes.getMethodHash(m);
      // add exceptions removing subclasses
      exceptions = removeSubclasses(m.getExceptionTypes());
    }

    public int compareTo(Object obj) {
      MethodRef that = (MethodRef) obj;
      int name = this.meth.getName().compareTo(that.meth.getName());
      if (name == 0) {
        return this.sig.compareTo(that.sig);
      }
      return name;
    }

    public boolean isMatch(Method m)
    {
      if (!meth.getName().equals(m.getName()))
        return false;

      Class[] params1 = meth.getParameterTypes();
      Class[] params2 = m.getParameterTypes();
      if (params1.length != params2.length)
        return false;

      for (int i = 0; i < params1.length; i++)
        if (!params1[i].equals(params2[i])) return false;

      return true;
    }

    private static List removeSubclasses(Class[] classes)
    {
      List list = new ArrayList();
      for (int i = 0; i < classes.length; i++)
        {
          Class candidate = classes[i];
          boolean add = true;
          for (int j = 0; j < classes.length; j++)
            {
              if (classes[j].equals(candidate))
                continue;
              else if (classes[j].isAssignableFrom(candidate))
                add = false;
            }
          if (add) list.add(candidate);
        }

      return list;
    }

    public void intersectExceptions(Method m)
    {
      List incoming = removeSubclasses(m.getExceptionTypes());

      List updated = new ArrayList();

      for (int i = 0; i < exceptions.size(); i++)
        {
          Class outer = (Class) exceptions.get(i);
          boolean addOuter = false;
          for (int j = 0; j < incoming.size(); j++)
            {
              Class inner = (Class) incoming.get(j);

              if (inner.equals(outer) || inner.isAssignableFrom(outer))
                addOuter = true;
              else if (outer.isAssignableFrom(inner))
                updated.add(inner);
            }

          if (addOuter)
            updated.add(outer);
        }

      exceptions = updated;
    }
  }
}
