/*
 * Copyright 2002,2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.proxy;

import com.intellij.ide.plugins.PluginManager;
import com.intellij.ide.plugins.cl.PluginClassLoader;
import com.intellij.util.ReflectionCache;
import net.sf.cglib.asm.ClassVisitor;
import net.sf.cglib.asm.Label;
import net.sf.cglib.asm.Type;
import net.sf.cglib.core.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Generates dynamic subclasses to enable method interception. This
 * class started as a substitute for the standard Dynamic Proxy support
 * included with JDK 1.3, but one that allowed the proxies to extend a
 * concrete base class, in addition to implementing interfaces. The dynamically
 * generated subclasses override the non-final methods of the superclass and
 * have hooks which callback to user-defined interceptor
 * implementations.
 * <p>
 * The original and most general callback type is the {@link MethodInterceptor}, which
 * in AOP terms enables "around advice"--that is, you can invoke custom code both before
 * and after the invocation of the "super" method. In addition you can modify the
 * arguments before calling the super method, or not call it at all.
 * <p>
 * Although <code>MethodInterceptor</code> is generic enough to meet any
 * interception need, it is often overkill. For simplicity and performance, additional
 * specialized callback types, such as {@link LazyLoader} are also available.
 * Often a single callback will be used per enhanced class, but you can control
 * which callback is used on a per-method basis with a {@link CallbackFilter}.
 * <p>
 * The most common uses of this class are embodied in the static helper methods. For
 * advanced needs, such as customizing the <code>ClassLoader</code> to use, you should create
 * a new instance of <code>Enhancer</code>. Other classes within CGLIB follow a similar pattern.
 * <p>
 * All enhanced objects implement the {@link Factory} interface, unless {@link #setUseFactory} is
 * used to explicitly disable this feature. The <code>Factory</code> interface provides an API
 * to change the callbacks of an existing object, as well as a faster and easier way to create
 * new instances of the same type.
 * <p>
 * For an almost drop-in replacement for
 * <code>java.lang.reflect.Proxy</code>, see the {@link Proxy} class.
 */

public class AdvancedEnhancer extends AbstractClassGenerator
{
  private static final CallbackFilter ALL_ZERO = new CallbackFilter(){
    public int accept(Method method) {
      return 0;
    }
  };

  private static final Source SOURCE = new Source(Enhancer.class.getName());
  private static final EnhancerKey KEY_FACTORY =
    (EnhancerKey)KeyFactory.create(EnhancerKey.class);

  private static final String BOUND_FIELD = "CGLIB$BOUND";
  private static final String THREAD_CALLBACKS_FIELD = "CGLIB$THREAD_CALLBACKS";
  private static final String STATIC_CALLBACKS_FIELD = "CGLIB$STATIC_CALLBACKS";
  private static final String SET_THREAD_CALLBACKS_NAME = "CGLIB$SET_THREAD_CALLBACKS";
  private static final String SET_STATIC_CALLBACKS_NAME = "CGLIB$SET_STATIC_CALLBACKS";
  private static final String CONSTRUCTED_FIELD = "CGLIB$CONSTRUCTED";

  private static final Type FACTORY =
    TypeUtils.parseType("net.sf.cglib.proxy.Factory");
  private static final Type ILLEGAL_STATE_EXCEPTION =
    TypeUtils.parseType("IllegalStateException");
  private static final Type ILLEGAL_ARGUMENT_EXCEPTION =
    TypeUtils.parseType("IllegalArgumentException");
  private static final Type THREAD_LOCAL =
    TypeUtils.parseType("ThreadLocal");
  private static final Type CALLBACK =
    TypeUtils.parseType("net.sf.cglib.proxy.Callback");
  private static final Type CALLBACK_ARRAY =
    Type.getType("[Lnet/sf/cglib/proxy/Callback;");
  private static final Signature CSTRUCT_NULL =
    TypeUtils.parseConstructor("");
  private static final Signature SET_THREAD_CALLBACKS =
    new Signature(SET_THREAD_CALLBACKS_NAME, Type.VOID_TYPE, new Type[]{ CALLBACK_ARRAY });
  private static final Signature SET_STATIC_CALLBACKS =
    new Signature(SET_STATIC_CALLBACKS_NAME, Type.VOID_TYPE, new Type[]{ CALLBACK_ARRAY });
  private static final Signature NEW_INSTANCE =
    new Signature("newInstance", Constants.TYPE_OBJECT, new Type[]{ CALLBACK_ARRAY });
  private static final Signature MULTIARG_NEW_INSTANCE =
    new Signature("newInstance", Constants.TYPE_OBJECT, new Type[]{
      Constants.TYPE_CLASS_ARRAY,
      Constants.TYPE_OBJECT_ARRAY,
      CALLBACK_ARRAY,
    });
  private static final Signature SINGLE_NEW_INSTANCE =
    new Signature("newInstance", Constants.TYPE_OBJECT, new Type[]{ CALLBACK });
  private static final Signature SET_CALLBACK =
    new Signature("setCallback", Type.VOID_TYPE, new Type[]{ Type.INT_TYPE, CALLBACK });
  private static final Signature GET_CALLBACK =
    new Signature("getCallback", CALLBACK, new Type[]{ Type.INT_TYPE });
  private static final Signature SET_CALLBACKS =
    new Signature("setCallbacks", Type.VOID_TYPE, new Type[]{ CALLBACK_ARRAY });
  private static final Signature GET_CALLBACKS =
    new Signature("getCallbacks", CALLBACK_ARRAY, new Type[0]);
  private static final Signature THREAD_LOCAL_GET =
    TypeUtils.parseSignature("Object get()");
  private static final Signature THREAD_LOCAL_SET =
    TypeUtils.parseSignature("void set(Object)");
  private static final Signature BIND_CALLBACKS =
    TypeUtils.parseSignature("void CGLIB$BIND_CALLBACKS(Object)");

  /** Internal interface, only public due to ClassLoader issues. */
  public interface EnhancerKey {
    public Object newInstance(String type,
                              String[] interfaces,
                              CallbackFilter filter,
                              Type[] callbackTypes,
                              boolean useFactory,
                              boolean interceptDuringConstruction,
                              Long serialVersionUID);
  }

  private Class[] interfaces;
  private CallbackFilter filter;
  private Callback[] callbacks;
  private Type[] callbackTypes;
  private boolean classOnly;
  private Class superclass;
  private Class[] argumentTypes;
  private Object[] arguments;
  private boolean useFactory = true;
  private Long serialVersionUID;
  private boolean interceptDuringConstruction = true;

  /**
   * Create a new <code>Enhancer</code>. A new <code>Enhancer</code>
   * object should be used for each generated object, and should not
   * be shared across threads. To create additional instances of a
   * generated class, use the <code>Factory</code> interface.
   * @see Factory
   */
  public AdvancedEnhancer() {
    super(SOURCE);
  }

  /**
   * Set the class which the generated class will extend. As a convenience,
   * if the supplied superclass is actually an interface, <code>setInterfaces</code>
   * will be called with the appropriate argument instead.
   * A non-interface argument must not be declared as final, and must have an
   * accessible constructor.
   * @param superclass class to extend or interface to implement
   * @see #setInterfaces(Class[])
   */
  public void setSuperclass(Class superclass) {
    if (superclass != null && superclass.isInterface()) {
      setInterfaces(new Class[]{ superclass });
    } else if (superclass != null && superclass.equals(Object.class)) {
      // affects choice of ClassLoader
      this.superclass = null;
    } else {
      this.superclass = superclass;
    }
  }

  /**
   * Set the interfaces to implement. The <code>Factory</code> interface will
   * always be implemented regardless of what is specified here.
   * @param interfaces array of interfaces to implement, or null
   * @see Factory
   */
  public void setInterfaces(Class[] interfaces) {
    this.interfaces = interfaces;
  }

  /**
   * Set the {@link CallbackFilter} used to map the generated class' methods
   * to a particular callback index.
   * New object instances will always use the same mapping, but may use different
   * actual callback objects.
   * @param filter the callback filter to use when generating a new class
   * @see #setCallbacks
   */
  public void setCallbackFilter(CallbackFilter filter) {
    this.filter = filter;
  }


  /**
   * Set the single {@link Callback} to use.
   * Ignored if you use {@link #createClass}.
   * @param callback the callback to use for all methods
   * @see #setCallbacks
   */
  public void setCallback(final Callback callback) {
    setCallbacks(new Callback[]{ callback });
  }

  /**
   * Set the array of callbacks to use.
   * Ignored if you use {@link #createClass}.
   * You must use a {@link CallbackFilter} to specify the index into this
   * array for each method in the proxied class.
   * @param callbacks the callback array
   * @see #setCallbackFilter
   * @see #setCallback
   */
  public void setCallbacks(Callback[] callbacks) {
    if (callbacks != null && callbacks.length == 0) {
      throw new IllegalArgumentException("Array cannot be empty");
    }
    this.callbacks = callbacks;
  }

  /**
   * Set whether the enhanced object instances should implement
   * the {@link Factory} interface.
   * This was added for tools that need for proxies to be more
   * indistinguishable from their targets. Also, in some cases it may
   * be necessary to disable the <code>Factory</code> interface to
   * prevent code from changing the underlying callbacks.
   * @param useFactory whether to implement <code>Factory</code>; default is <code>true</code>
   */
  public void setUseFactory(boolean useFactory) {
    this.useFactory = useFactory;
  }

  /**
   * Set whether methods called from within the proxy's constructer
   * will be intercepted. The default value is true. Unintercepted methods
   * will call the method of the proxy's base class, if it exists.
   * @param interceptDuringConstruction whether to intercept methods called from the constructor
   */
  public void setInterceptDuringConstruction(boolean interceptDuringConstruction) {
    this.interceptDuringConstruction = interceptDuringConstruction;
  }

  /**
   * Set the single type of {@link Callback} to use.
   * This may be used instead of {@link #setCallback} when calling
   * {@link #createClass}, since it may not be possible to have
   * an array of actual callback instances.
   * @param callbackType the type of callback to use for all methods
   * @see #setCallbackTypes
   */
  public void setCallbackType(Class callbackType) {
    setCallbackTypes(new Class[]{ callbackType });
  }

  /**
   * Set the array of callback types to use.
   * This may be used instead of {@link #setCallbacks} when calling
   * {@link #createClass}, since it may not be possible to have
   * an array of actual callback instances.
   * You must use a {@link CallbackFilter} to specify the index into this
   * array for each method in the proxied class.
   * @param callbackTypes the array of callback types
   */
  public void setCallbackTypes(Class[] callbackTypes) {
    if (callbackTypes != null && callbackTypes.length == 0) {
      throw new IllegalArgumentException("Array cannot be empty");
    }
    this.callbackTypes = CallbackInfo.determineTypes(callbackTypes);
  }

  /**
   * Generate a new class if necessary and uses the specified
   * callbacks (if any) to create a new object instance.
   * Uses the no-arg constructor of the superclass.
   * @return a new instance
   */
  public Object create() {
    classOnly = false;
    argumentTypes = null;
    return createHelper();
  }

  /**
   * Generate a new class if necessary and uses the specified
   * callbacks (if any) to create a new object instance.
   * Uses the constructor of the superclass matching the <code>argumentTypes</code>
   * parameter, with the given arguments.
   * @param argumentTypes constructor signature
   * @param arguments compatible wrapped arguments to pass to constructor
   * @return a new instance
   */
  public Object create(Class[] argumentTypes, Object[] arguments) {
    classOnly = false;
    if (argumentTypes == null || arguments == null || argumentTypes.length != arguments.length) {
      throw new IllegalArgumentException("Arguments must be non-null and of equal length");
    }
    synchronized (this) {
      this.argumentTypes = argumentTypes;
      this.arguments = arguments;
      try {
        return createHelper();
      }
      finally {
        this.arguments = null;
      }
    }
  }

  /**
   * Generate a new class if necessary and return it without creating a new instance.
   * This ignores any callbacks that have been set.
   * To create a new instance you will have to use reflection, and methods
   * called during the constructor will not be intercepted. To avoid this problem,
   * use the multi-arg <code>create</code> method.
   * @see #create(Class[], Object[])
   */
  public Class createClass() {
    classOnly = true;
    return (Class)createHelper();
  }

  /**
   * Insert a static serialVersionUID field into the generated class.
   * @param sUID the field value, or null to avoid generating field.
   */
  public void setSerialVersionUID(Long sUID) {
    serialVersionUID = sUID;
  }

  private void validate() {
    if (classOnly ^ (callbacks == null)) {
      if (classOnly) {
        throw new IllegalStateException("createClass does not accept callbacks");
      } else {
        throw new IllegalStateException("Callbacks are required");
      }
    }
    if (classOnly && (callbackTypes == null)) {
      throw new IllegalStateException("Callback types are required");
    }
    if (callbacks != null && callbackTypes != null) {
      if (callbacks.length != callbackTypes.length) {
        throw new IllegalStateException("Lengths of callback and callback types array must be the same");
      }
      Type[] check = CallbackInfo.determineTypes(callbacks);
      for (int i = 0; i < check.length; i++) {
        if (!check[i].equals(callbackTypes[i])) {
          throw new IllegalStateException("Callback " + check[i] + " is not assignable to " + callbackTypes[i]);
        }
      }
    } else if (callbacks != null) {
      callbackTypes = CallbackInfo.determineTypes(callbacks);
    }
    if (filter == null) {
      if (callbackTypes.length > 1) {
        throw new IllegalStateException("Multiple callback types possible but no filter specified");
      }
      filter = ALL_ZERO;
    }
    if (interfaces != null) {
      for (Class anInterface : interfaces) {
        if (anInterface == null) {
          throw new IllegalStateException("Interfaces cannot be null");
        }
        if (!anInterface.isInterface()) {
          throw new IllegalStateException(anInterface + " is not an interface");
        }
      }
    }
  }

  private Object createHelper() {
    validate();
    if (superclass != null) {
      setNamePrefix(superclass.getName());
    } else if (interfaces != null) {
      setNamePrefix(interfaces[ReflectUtils.findPackageProtected(interfaces)].getName());
    }
    return super.create(KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
                                                ReflectUtils.getNames(interfaces),
                                                filter,
                                                callbackTypes,
                                                useFactory,
                                                interceptDuringConstruction,
                                                serialVersionUID));
  }

  protected ClassLoader getDefaultClassLoader() {
    int maxIndex = -1;
    ClassLoader bestLoader = null;
    if (interfaces != null && interfaces.length > 0) {
      for (final Class anInterface : interfaces) {
        final ClassLoader loader = anInterface.getClassLoader();
        if (loader instanceof PluginClassLoader) {
          final int order = PluginManager.getPluginLoadingOrder(((PluginClassLoader)loader).getPluginId());
          if (maxIndex < order) {
            maxIndex = order;
            bestLoader = loader;
          }
        }
      }
    }
    ClassLoader superLoader = null;
    if (superclass != null) {
      superLoader = superclass.getClassLoader();
      if (superLoader instanceof PluginClassLoader &&
          maxIndex < PluginManager.getPluginLoadingOrder(((PluginClassLoader)superLoader).getPluginId())) {
        return superLoader;
      }
    }
    if (bestLoader != null) return bestLoader;
    return superLoader;
  }

  private static Signature rename(Signature sig, int index) {
    return new Signature("CGLIB$" + sig.getName() + "$" + index,
                         sig.getDescriptor());
  }

  /**
   * Finds all of the methods that will be extended by an
   * Enhancer-generated class using the specified superclass and
   * interfaces. This can be useful in building a list of Callback
   * objects. The methods are added to the end of the given list.  Due
   * to the subclassing nature of the classes generated by Enhancer,
   * the methods are guaranteed to be non-static, non-final, and
   * non-private. Each method signature will only occur once, even if
   * it occurs in multiple classes.
   * @param superclass the class that will be extended, or null
   * @param interfaces the list of interfaces that will be implemented, or null
   * @param methods the list into which to copy the applicable methods
   */
  public static void getMethods(Class superclass, Class[] interfaces, List<Method> methods)
  {
    getMethods(superclass, interfaces, methods, null, null);
  }

  private static void getMethods(Class superclass, Class[] interfaces, List<Method> methods, List<Method> interfaceMethods, Set forcePublic)
  {
    ReflectUtils.addAllMethods(superclass, methods);
    List<Method> target = (interfaceMethods != null) ? interfaceMethods : methods;
    if (interfaces != null) {
      for (Class anInterface : interfaces) {
        if (anInterface != Factory.class) {
          ReflectUtils.addAllMethods(anInterface, target);
        }
      }
    }
    if (interfaceMethods != null) {
      if (forcePublic != null) {
        forcePublic.addAll(MethodWrapper.createSet(interfaceMethods));
      }
      methods.addAll(interfaceMethods);
    }
    CollectionUtils.filter(methods, new RejectModifierPredicate(Constants.ACC_STATIC | Constants.ACC_FINAL));
    CollectionUtils.filter(methods, new VisibilityPredicate(superclass, true));
    CollectionUtils.filter(methods, new DuplicatesPredicate());
  }

  public void generateClass(ClassVisitor v) throws Exception {
    Class sc = (superclass == null) ? Object.class : superclass;

    if (TypeUtils.isFinal(sc.getModifiers())) {
      throw new IllegalArgumentException("Cannot subclass final class " + sc);
    }
    List<Constructor> constructors = new ArrayList<Constructor>(Arrays.asList(sc.getDeclaredConstructors()));
    filterConstructors(sc, constructors);

    // Order is very important: must add superclass, then
    // its superclass chain, then each interface and
    // its superinterfaces.
    final Set forcePublic = new HashSet();
    List<Method> actualMethods = new ArrayList<Method>();
    final Map<Method, Method> covariantMethods = new HashMap<Method, Method>();
    getMethods(sc, interfaces, actualMethods, new ArrayList<Method>(), forcePublic);

    //Changes by Peter Gromov & Gregory Shrago

    for(Class aClass = sc; aClass != null; aClass = aClass.getSuperclass()) {
      for (final Method method : aClass.getDeclaredMethods()) {
        if (actualMethods.contains(method)) {
          removeAllCovariantMethods(actualMethods, method, covariantMethods);
        }
      }
    }


    ClassEmitter e = new ClassEmitter(v);
    e.begin_class(Constants.V1_2,
                  Constants.ACC_PUBLIC,
                  getClassName(),
                  Type.getType(sc),
                  (useFactory ?
                   TypeUtils.add(TypeUtils.getTypes(interfaces), FACTORY) :
                   TypeUtils.getTypes(interfaces)),
                  Constants.SOURCE_FILE);
    List constructorInfo = CollectionUtils.transform(constructors, MethodInfoTransformer.getInstance());

    e.declare_field(Constants.ACC_PRIVATE, BOUND_FIELD, Type.BOOLEAN_TYPE, null, null);
    if (!interceptDuringConstruction) {
      e.declare_field(Constants.ACC_PRIVATE, CONSTRUCTED_FIELD, Type.BOOLEAN_TYPE, null, null);
    }
    e.declare_field(Constants.PRIVATE_FINAL_STATIC, THREAD_CALLBACKS_FIELD, THREAD_LOCAL, null, null);
    e.declare_field(Constants.PRIVATE_FINAL_STATIC, STATIC_CALLBACKS_FIELD, CALLBACK_ARRAY, null, null);
    if (serialVersionUID != null) {
      e.declare_field(Constants.PRIVATE_FINAL_STATIC, Constants.SUID_FIELD_NAME, Type.LONG_TYPE, serialVersionUID, null);
    }

    for (int i = 0; i < callbackTypes.length; i++) {
      e.declare_field(Constants.ACC_PRIVATE, getCallbackField(i), callbackTypes[i], null, null);
    }
    final Map<Method, MethodInfo> methodInfoMap = new HashMap<Method, MethodInfo>();
    for (Method method : actualMethods) {
      int modifiers =
        Constants.ACC_FINAL | (method.getModifiers() & ~Constants.ACC_ABSTRACT & ~Constants.ACC_NATIVE & ~Constants.ACC_SYNCHRONIZED);
      if (forcePublic.contains(MethodWrapper.create(method))) {
        modifiers = (modifiers & ~Constants.ACC_PROTECTED) | Constants.ACC_PUBLIC;
      }
      if (covariantMethods.containsKey(method)) {
        modifiers = modifiers | Constants.ACC_BRIDGE;
      }
      methodInfoMap.put(method, ReflectUtils.getMethodInfo(method, modifiers));
    }

    emitMethods(e, methodInfoMap, covariantMethods);
    emitConstructors(e, constructorInfo);
    emitSetThreadCallbacks(e);
    emitSetStaticCallbacks(e);
    emitBindCallbacks(e);

    if (useFactory) {
      int[] keys = getCallbackKeys();
      emitNewInstanceCallbacks(e);
      emitNewInstanceCallback(e);
      emitNewInstanceMultiarg(e, constructorInfo);
      emitGetCallback(e, keys);
      emitSetCallback(e, keys);
      emitGetCallbacks(e);
      emitSetCallbacks(e);
    }

    e.end_class();
  }

  private static void removeAllCovariantMethods(final List<Method> actualMethods, final Method method, final Map<Method, Method> covariantMethods) {
    if ((method.getModifiers() & Constants.ACC_SYNTHETIC) != 0) {
      return;
    }

    for (Iterator<Method> it = actualMethods.iterator(); it.hasNext();) {
      Method actualMethod = it.next();
      if (actualMethod.equals(method)) {
        continue;
      }

      if (!actualMethod.getName().equals(method.getName()) ||
          !Arrays.equals(actualMethod.getParameterTypes(), method.getParameterTypes())) {
        continue;
      }

      if (ReflectionCache.isAssignable(actualMethod.getReturnType(), method.getReturnType())) {
        if ((actualMethod.getModifiers() & Constants.ACC_ABSTRACT) != 0 || (actualMethod.getModifiers() & Constants.ACC_SYNTHETIC) != 0) {
          covariantMethods.put(actualMethod, method); //generate bridge
        }
        else {
          it.remove();
        }
      }
    }
  }

  /**
   * Filter the list of constructors from the superclass. The
   * constructors which remain will be included in the generated
   * class. The default implementation is to filter out all private
   * constructors, but subclasses may extend Enhancer to override this
   * behavior.
   * @param sc the superclass
   * @param constructors the list of all declared constructors from the superclass
   * @throws IllegalArgumentException if there are no non-private constructors
   */
  protected void filterConstructors(Class sc, List<Constructor> constructors) {
    CollectionUtils.filter(constructors, new VisibilityPredicate(sc, true));
    if (constructors.size() == 0) {
      throw new IllegalArgumentException("No visible constructors in " + sc);
    }
  }

  protected Object firstInstance(Class type) throws Exception {
    if (classOnly) {
      return type;
    } else {
      return createUsingReflection(type);
    }
  }

  protected Object nextInstance(Object instance) {
    Class protoclass = (instance instanceof Class) ? (Class) instance : instance.getClass();
    if (classOnly) {
      return protoclass;
    } else if (instance instanceof Factory) {
      if (argumentTypes != null) {
        return ((Factory)instance).newInstance(argumentTypes, arguments, callbacks);
      } else {
        return ((Factory)instance).newInstance(callbacks);
      }
    } else {
      return createUsingReflection(protoclass);
    }
  }

  /**
   * Call this method to register the {@link Callback} array to use before
   * creating a new instance of the generated class via reflection. If you are using
   * an instance of <code>Enhancer</code> or the {@link Factory} interface to create
   * new instances, this method is unnecessary. Its primary use is for when you want to
   * cache and reuse a generated class yourself, and the generated class does
   * <i>not</i> implement the {@link Factory} interface.
   * <p>
   * Note that this method only registers the callbacks on the current thread.
   * If you want to register callbacks for instances created by multiple threads,
   * use {@link #registerStaticCallbacks}.
   * <p>
   * The registered callbacks are overwritten and subsequently cleared
   * when calling any of the <code>create</code> methods (such as
   * {@link #create}).
   * @param generatedClass a class previously created by {@link Enhancer}
   * @param callbacks the array of callbacks to use when instances of the generated
   * class are created
   * @see #setUseFactory
   */
  public static void registerCallbacks(Class generatedClass, Callback[] callbacks) {
    setThreadCallbacks(generatedClass, callbacks);
  }

  /**
   * Similar to {@link #registerCallbacks}, but suitable for use
   * when multiple threads will be creating instances of the generated class.
   * The thread-level callbacks will always override the static callbacks.
   * Static callbacks are never cleared.
   * @param generatedClass a class previously created by {@link Enhancer}
   * @param callbacks the array of callbacks to use when instances of the generated
   * class are created
   */
  public static void registerStaticCallbacks(Class generatedClass, Callback[] callbacks) {
    setCallbacksHelper(generatedClass, callbacks, SET_STATIC_CALLBACKS_NAME);
  }

  /**
   * Determine if a class was generated using <code>Enhancer</code>.
   * @param type any class
   * @return whether the class was generated  using <code>Enhancer</code>
   */
  public static boolean isEnhanced(Class type) {
    try {
      getCallbacksSetter(type, SET_THREAD_CALLBACKS_NAME);
      return true;
    } catch (NoSuchMethodException e) {
      return false;
    }
  }

  private static void setThreadCallbacks(Class type, Callback[] callbacks) {
    setCallbacksHelper(type, callbacks, SET_THREAD_CALLBACKS_NAME);
  }

  private static void setCallbacksHelper(Class type, Callback[] callbacks, String methodName) {
    // TODO: optimize
    try {
      Method setter = getCallbacksSetter(type, methodName);
      setter.invoke(null, (Object)callbacks);
    } catch (NoSuchMethodException e) {
      throw new IllegalArgumentException(type + " is not an enhanced class");
    } catch (IllegalAccessException e) {
      throw new CodeGenerationException(e);
    } catch (InvocationTargetException e) {
      throw new CodeGenerationException(e);
    }
  }

  private static Method getCallbacksSetter(Class type, String methodName) throws NoSuchMethodException {
    return type.getDeclaredMethod(methodName, Callback[].class);
  }

  private Object createUsingReflection(Class type) {
    setThreadCallbacks(type, callbacks);
    try{

      if (argumentTypes != null) {

        return ReflectUtils.newInstance(type, argumentTypes, arguments);

      } else {

        return ReflectUtils.newInstance(type);

      }
    }finally{
      // clear thread callbacks to allow them to be gc'd
      setThreadCallbacks(type, null);
    }
  }

  /**
   * Helper method to create an intercepted object.
   * For finer control over the generated instance, use a new instance of <code>Enhancer</code>
   * instead of this static method.
   * @param type class to extend or interface to implement
   * @param callback the callback to use for all methods
   */
  public static Object create(Class type, Callback callback) {
    Enhancer e = new Enhancer();
    e.setSuperclass(type);
    e.setCallback(callback);
    return e.create();
  }

  /**
   * Helper method to create an intercepted object.
   * For finer control over the generated instance, use a new instance of <code>Enhancer</code>
   * instead of this static method.
   * @param type class to extend or interface to implement
   * @param interfaces array of interfaces to implement, or null
   * @param callback the callback to use for all methods
   */
  public static Object create(Class superclass, Class interfaces[], Callback callback) {
    Enhancer e = new Enhancer();
    e.setSuperclass(superclass);
    e.setInterfaces(interfaces);
    e.setCallback(callback);
    return e.create();
  }

  /**
   * Helper method to create an intercepted object.
   * For finer control over the generated instance, use a new instance of <code>Enhancer</code>
   * instead of this static method.
   * @param type class to extend or interface to implement
   * @param interfaces array of interfaces to implement, or null
   * @param filter the callback filter to use when generating a new class
   * @param callbacks callback implementations to use for the enhanced object
   */
  public static Object create(Class superclass, Class[] interfaces, CallbackFilter filter, Callback[] callbacks) {
    Enhancer e = new Enhancer();
    e.setSuperclass(superclass);
    e.setInterfaces(interfaces);
    e.setCallbackFilter(filter);
    e.setCallbacks(callbacks);
    return e.create();
  }

  private void emitConstructors(ClassEmitter ce, List constructors) {
    boolean seenNull = false;
    for (final Object constructor1 : constructors) {
      MethodInfo constructor = (MethodInfo)constructor1;
      CodeEmitter e = EmitUtils.begin_method(ce, constructor, Constants.ACC_PUBLIC);
      e.load_this();
      e.dup();
      e.load_args();
      Signature sig = constructor.getSignature();
      seenNull = seenNull || sig.getDescriptor().equals("()V");
      e.super_invoke_constructor(sig);
      e.invoke_static_this(BIND_CALLBACKS);
      if (!interceptDuringConstruction) {
        e.load_this();
        e.push(1);
        e.putfield(CONSTRUCTED_FIELD);
      }
      e.return_value();
      e.end_method();
    }
    if (!classOnly && !seenNull && arguments == null) {
      throw new IllegalArgumentException("Superclass has no null constructors but no arguments were given");
    }
  }

  private int[] getCallbackKeys() {
    int[] keys = new int[callbackTypes.length];
    for (int i = 0; i < callbackTypes.length; i++) {
      keys[i] = i;
    }
    return keys;
  }

  private void emitGetCallback(ClassEmitter ce, int[] keys) {
    final CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, GET_CALLBACK, null, null);
    e.load_this();
    e.invoke_static_this(BIND_CALLBACKS);
    e.load_this();
    e.load_arg(0);
    e.process_switch(keys, new ProcessSwitchCallback() {
      public void processCase(int key, Label end) {
        e.getfield(getCallbackField(key));
        e.goTo(end);
      }
      public void processDefault() {
        e.pop(); // stack height
        e.aconst_null();
      }
    });
    e.return_value();
    e.end_method();
  }

  private void emitSetCallback(ClassEmitter ce, int[] keys) {
    final CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, SET_CALLBACK, null, null);
    e.load_this();
    e.load_arg(1);
    e.load_arg(0);
    e.process_switch(keys, new ProcessSwitchCallback() {
      public void processCase(int key, Label end) {
        e.checkcast(callbackTypes[key]);
        e.putfield(getCallbackField(key));
        e.goTo(end);
      }
      public void processDefault() {
        final Type type = Type.getType(AssertionError.class);
        e.new_instance(type);
        e.dup();
        e.invoke_constructor(type);
        e.athrow();
      }
    });
    e.return_value();
    e.end_method();
  }

  private void emitSetCallbacks(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, SET_CALLBACKS, null, null);
    e.load_this();
    e.load_arg(0);
    for (int i = 0; i < callbackTypes.length; i++) {
      e.dup2();
      e.aaload(i);
      e.checkcast(callbackTypes[i]);
      e.putfield(getCallbackField(i));
    }
    e.return_value();
    e.end_method();
  }

  private void emitGetCallbacks(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, GET_CALLBACKS, null, null);
    e.load_this();
    e.invoke_static_this(BIND_CALLBACKS);
    e.load_this();
    e.push(callbackTypes.length);
    e.newarray(CALLBACK);
    for (int i = 0; i < callbackTypes.length; i++) {
      e.dup();
      e.push(i);
      e.load_this();
      e.getfield(getCallbackField(i));
      e.aastore();
    }
    e.return_value();
    e.end_method();
  }

  private void emitNewInstanceCallbacks(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, NEW_INSTANCE, null, null);
    e.load_arg(0);
    e.invoke_static_this(SET_THREAD_CALLBACKS);
    emitCommonNewInstance(e);
  }

  private void emitCommonNewInstance(CodeEmitter e) {
    e.new_instance_this();
    e.dup();
    e.invoke_constructor_this();
    e.aconst_null();
    e.invoke_static_this(SET_THREAD_CALLBACKS);
    e.return_value();
    e.end_method();
  }

  private void emitNewInstanceCallback(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, SINGLE_NEW_INSTANCE, null, null);
    switch (callbackTypes.length) {
      case 0:
        // TODO: make sure Callback is null
        break;
      case 1:
        // for now just make a new array; TODO: optimize
        e.push(1);
        e.newarray(CALLBACK);
        e.dup();
        e.push(0);
        e.load_arg(0);
        e.aastore();
        e.invoke_static_this(SET_THREAD_CALLBACKS);
        break;
      default:
        e.throw_exception(ILLEGAL_STATE_EXCEPTION, "More than one callback object required");
    }
    emitCommonNewInstance(e);
  }

  private void emitNewInstanceMultiarg(ClassEmitter ce, List constructors) {
    final CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, MULTIARG_NEW_INSTANCE, null, null);
    e.load_arg(2);
    e.invoke_static_this(SET_THREAD_CALLBACKS);
    e.new_instance_this();
    e.dup();
    e.load_arg(0);
    EmitUtils.constructor_switch(e, constructors, new ObjectSwitchCallback() {
      public void processCase(Object key, Label end) {
        MethodInfo constructor = (MethodInfo)key;
        Type types[] = constructor.getSignature().getArgumentTypes();
        for (int i = 0; i < types.length; i++) {
          e.load_arg(1);
          e.push(i);
          e.aaload();
          e.unbox(types[i]);
        }
        e.invoke_constructor_this(constructor.getSignature());
        e.goTo(end);
      }
      public void processDefault() {
        e.throw_exception(ILLEGAL_ARGUMENT_EXCEPTION, "Constructor not found");
      }
    });
    e.aconst_null();
    e.invoke_static_this(SET_THREAD_CALLBACKS);
    e.return_value();
    e.end_method();
  }

  private void emitMethods(final ClassEmitter ce, Map<Method, MethodInfo> methodMap, final Map<Method, Method> covariantMethods) {
    CallbackGenerator[] generators = CallbackInfo.getGenerators(callbackTypes);
    Map<MethodInfo, MethodInfo> covariantInfoMap = new HashMap<MethodInfo, MethodInfo>();
    for (Method method : methodMap.keySet()) {
      final Method delegate = covariantMethods.get(method);
      if (delegate != null) {
        covariantInfoMap.put(methodMap.get(method), ReflectUtils.getMethodInfo(delegate, delegate.getModifiers()));
      }
    }
    BridgeMethodGenerator bridgeMethodGenerator = new BridgeMethodGenerator(covariantInfoMap);

    Map<CallbackGenerator,List<MethodInfo>> groups = new HashMap<CallbackGenerator, List<MethodInfo>>();
    final Map<MethodInfo,Integer> indexes = new HashMap<MethodInfo, Integer>();
    final Map<MethodInfo,Integer> originalModifiers = new HashMap<MethodInfo, Integer>();
    final Map positions = CollectionUtils.getIndexMap(new ArrayList<MethodInfo>(methodMap.values()));

    for (Method actualMethod : methodMap.keySet()) {
      MethodInfo method = methodMap.get(actualMethod);
      int index = filter.accept(actualMethod);
      if (index >= callbackTypes.length) {
        throw new IllegalArgumentException("Callback filter returned an index that is too large: " + index);
      }
      originalModifiers.put(method, (actualMethod != null) ? actualMethod.getModifiers() : method.getModifiers());
      indexes.put(method, index);
      final CallbackGenerator generator = covariantMethods.containsKey(actualMethod)? bridgeMethodGenerator : generators[index];
      List<MethodInfo> group = groups.get(generator);
      if (group == null) {
        groups.put(generator, group = new ArrayList<MethodInfo>(methodMap.size()));
      }
      group.add(method);
    }

    CodeEmitter se = ce.getStaticHook();
    se.new_instance(THREAD_LOCAL);
    se.dup();
    se.invoke_constructor(THREAD_LOCAL, CSTRUCT_NULL);
    se.putfield(THREAD_CALLBACKS_FIELD);

    CallbackGenerator.Context context = new CallbackGenerator.Context() {
      public int getOriginalModifiers(MethodInfo method) {
        return originalModifiers.get(method);
      }
      public int getIndex(MethodInfo method) {
        return indexes.get(method);
      }
      public void emitCallback(CodeEmitter e, int index) {
        emitCurrentCallback(e, index);
      }
      public Signature getImplSignature(MethodInfo method) {
        return rename(method.getSignature(), (Integer)positions.get(method));
      }
      public CodeEmitter beginMethod(ClassEmitter ce, MethodInfo method) {
        CodeEmitter e = EmitUtils.begin_method(ce, method);
        if (!interceptDuringConstruction &&
            !TypeUtils.isAbstract(method.getModifiers())) {
          Label constructed = e.make_label();
          e.load_this();
          e.getfield(CONSTRUCTED_FIELD);
          e.if_jump(e.NE, constructed);
          e.load_this();
          e.load_args();
          e.super_invoke();
          e.return_value();
          e.mark(constructed);
        }
        return e;
      }
    };
    Set<CallbackGenerator> seenGen = new HashSet<CallbackGenerator>();
    for (int i = 0; i < callbackTypes.length + 1; i++) {
      CallbackGenerator gen = i == callbackTypes.length? bridgeMethodGenerator : generators[i];
      if (!seenGen.contains(gen)) {
        seenGen.add(gen);
        final List<MethodInfo> fmethods = groups.get(gen);
        if (fmethods != null) {
          try {
            gen.generate(ce, context, fmethods);
            gen.generateStatic(se, context, fmethods);
          } catch (RuntimeException x) {
            throw x;
          } catch (Exception x) {
            throw new CodeGenerationException(x);
          }
        }
      }
    }
    se.return_value();
    se.end_method();
  }

  private void emitSetThreadCallbacks(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC | Constants.ACC_STATIC,
                                    SET_THREAD_CALLBACKS,
                                    null,
                                    null);
    e.getfield(THREAD_CALLBACKS_FIELD);
    e.load_arg(0);
    e.invoke_virtual(THREAD_LOCAL, THREAD_LOCAL_SET);
    e.return_value();
    e.end_method();
  }

  private void emitSetStaticCallbacks(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC | Constants.ACC_STATIC,
                                    SET_STATIC_CALLBACKS,
                                    null,
                                    null);
    e.load_arg(0);
    e.putfield(STATIC_CALLBACKS_FIELD);
    e.return_value();
    e.end_method();
  }

  private void emitCurrentCallback(CodeEmitter e, int index) {
    e.load_this();
    e.getfield(getCallbackField(index));
    e.dup();
    Label end = e.make_label();
    e.ifnonnull(end);
    e.pop(); // stack height
    e.load_this();
    e.invoke_static_this(BIND_CALLBACKS);
    e.load_this();
    e.getfield(getCallbackField(index));
    e.mark(end);
  }

  private void emitBindCallbacks(ClassEmitter ce) {
    CodeEmitter e = ce.begin_method(Constants.PRIVATE_FINAL_STATIC,
                                    BIND_CALLBACKS,
                                    null,
                                    null);
    Local me = e.make_local();
    e.load_arg(0);
    e.checkcast_this();
    e.store_local(me);

    Label end = e.make_label();
    e.load_local(me);
    e.getfield(BOUND_FIELD);
    e.if_jump(e.NE, end);
    e.load_local(me);
    e.push(1);
    e.putfield(BOUND_FIELD);

    e.getfield(THREAD_CALLBACKS_FIELD);
    e.invoke_virtual(THREAD_LOCAL, THREAD_LOCAL_GET);
    e.dup();
    Label found_callback = e.make_label();
    e.ifnonnull(found_callback);
    e.pop();

    e.getfield(STATIC_CALLBACKS_FIELD);
    e.dup();
    e.ifnonnull(found_callback);
    e.pop();
    e.goTo(end);

    e.mark(found_callback);
    e.checkcast(CALLBACK_ARRAY);
    e.load_local(me);
    e.swap();
    for (int i = callbackTypes.length - 1; i >= 0; i--) {
      if (i != 0) {
        e.dup2();
      }
      e.aaload(i);
      e.checkcast(callbackTypes[i]);
      e.putfield(getCallbackField(i));
    }

    e.mark(end);
    e.return_value();
    e.end_method();
  }

  private static String getCallbackField(int index) {
    return "CGLIB$CALLBACK_" + index;
  }
}
