/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.openjpa.util;

import java.io.File;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.util.asm.AsmHelper;
import org.apache.openjpa.util.asm.ClassWriterTracker;
import org.apache.openjpa.util.proxy.DelayedArrayListProxy;
import org.apache.openjpa.util.proxy.DelayedHashSetProxy;
import org.apache.openjpa.util.proxy.DelayedLinkedHashSetProxy;
import org.apache.openjpa.util.proxy.DelayedLinkedListProxy;
import org.apache.openjpa.util.proxy.DelayedPriorityQueueProxy;
import org.apache.openjpa.util.proxy.DelayedTreeSetProxy;
import org.apache.openjpa.util.proxy.DelayedVectorProxy;
import org.apache.openjpa.util.proxy.ProxyBean;
import org.apache.openjpa.util.proxy.ProxyCalendar;
import org.apache.openjpa.util.proxy.ProxyCollection;
import org.apache.openjpa.util.proxy.ProxyCollections;
import org.apache.openjpa.util.proxy.ProxyDate;
import org.apache.openjpa.util.proxy.ProxyMap;
import org.apache.openjpa.util.proxy.ProxyMaps;
import org.apache.xbean.asm9.ClassWriter;
import org.apache.xbean.asm9.Label;
import org.apache.xbean.asm9.MethodVisitor;
import org.apache.xbean.asm9.Opcodes;
import org.apache.xbean.asm9.Type;


/**
 * Default implementation of the {@link ProxyManager} interface.
 *
 * @author Abe White
 * @author Mark Struberg
 */
public class ProxyManagerImpl
    implements ProxyManager {

    private static final String PROXY_SUFFIX = "$proxy";

    private static final Localizer _loc = Localizer.forPackage
        (ProxyManagerImpl.class);
    public static final Type TYPE_OBJECT = Type.getType(Object.class);

    private static long _proxyId = 0L;
    private static final Map _stdCollections = new HashMap();
    private static final Map _stdMaps = new HashMap();
    static {
        _stdCollections.put(Collection.class, ArrayList.class);
        _stdCollections.put(Set.class, HashSet.class);
        _stdCollections.put(SortedSet.class, TreeSet.class);
        _stdCollections.put(List.class, ArrayList.class);
        _stdCollections.put(Queue.class, LinkedList.class);
        _stdMaps.put(Map.class, HashMap.class);
        _stdMaps.put(SortedMap.class, TreeMap.class);
    }

    private final Set<String> _unproxyable = new HashSet<>();
    private final Map<Class<?>, Proxy> _proxies = new ConcurrentHashMap<>();
    private boolean _trackChanges = true;
    private boolean _assertType = false;
    private boolean _delayedCollectionLoading = false;

    public ProxyManagerImpl() {
        _unproxyable.add(TimeZone.class.getName());
    }

    /**
     * Whether proxies produced by this factory will use {@link ChangeTracker}s
     * to try to cut down on data store operations at the cost of some extra
     * bookkeeping overhead. Defaults to true.
     */
    public boolean getTrackChanges() {
        return _trackChanges;
    }

    /**
     * Whether proxies produced by this factory will use {@link ChangeTracker}s
     * to try to cut down on data store operations at the cost of some extra
     * bookkeeping overhead. Defaults to true.
     */
    public void setTrackChanges(boolean track) {
        _trackChanges = track;
    }

    /**
     * Whether to perform runtime checks to ensure that all elements
     * added to collection and map proxies are the proper element/key/value
     * type as defined by the metadata. Defaults to false.
     */
    public boolean getAssertAllowedType() {
        return _assertType;
    }

    /**
     * Whether to perform runtime checks to ensure that all elements
     * added to collection and map proxies are the proper element/key/value
     * type as defined by the metadata. Defaults to false.
     */
    public void setAssertAllowedType(boolean assertType) {
        _assertType = assertType;
    }

    /**
     * Whether loading of collections should be delayed until an operation
     * is performed that requires them to be loaded.  This property only
     * applies to proxies that implement java.util.Collection (ie. not arrays
     * or maps).  Defaults to false.
     */
    @Override
    public boolean getDelayCollectionLoading() {
        return _delayedCollectionLoading;
    }

    /**
     * Whether loading of collections should be delayed until an operation
     * is performed that requires them to be loaded.  Defaults to false.
     */
    public void setDelayCollectionLoading(boolean delay) {
        _delayedCollectionLoading = delay;
    }

    /**
     * Return a mutable view of class names we know cannot be proxied
     * correctly by this manager.
     */
    public Collection getUnproxyable() {
        return _unproxyable;
    }

    /**
     * Provided for auto-configuration.  Add the given semicolon-separated
     * class names to the set of class names we know cannot be proxied correctly
     * by this manager.
     */
    public void setUnproxyable(String clsNames) {
        if (clsNames != null)
            _unproxyable.addAll(Arrays.asList(StringUtil.split(clsNames, ";", 0)));
    }

    @Override
    public Object copyArray(Object orig) {
        if (orig == null)
            return null;

        try {
            int length = Array.getLength(orig);
            Object array = Array.newInstance(orig.getClass().
                getComponentType(), length);

            System.arraycopy(orig, 0, array, 0, length);
            return array;
        } catch (Exception e) {
            throw new UnsupportedException(_loc.get("bad-array",
                e.getMessage()), e);
        }
    }

    @Override
    public Collection copyCollection(Collection orig) {
        if (orig == null)
            return null;
        if (orig instanceof Proxy)
            return (Collection) ((Proxy) orig).copy(orig);

        ProxyCollection proxy = getFactoryProxyCollection(orig.getClass());
        return (Collection) proxy.copy(orig);
    }

    @Override
    public Proxy newCollectionProxy(Class type, Class elementType,
        Comparator compare, boolean autoOff) {
        type = toProxyableCollectionType(type);
        ProxyCollection proxy = getFactoryProxyCollection(type);
        return proxy.newInstance((_assertType) ? elementType : null, compare,
            _trackChanges, autoOff);
    }

    @Override
    public Map copyMap(Map orig) {
        if (orig == null)
            return null;
        if (orig instanceof Proxy)
            return (Map) ((Proxy) orig).copy(orig);

        ProxyMap proxy = getFactoryProxyMap(orig.getClass());
        return (Map) proxy.copy(orig);
    }

    @Override
    public Proxy newMapProxy(Class type, Class keyType,
        Class elementType, Comparator compare,boolean autoOff) {
        type = toProxyableMapType(type);
        ProxyMap proxy = getFactoryProxyMap(type);
        return proxy.newInstance((_assertType) ? keyType : null,
            (_assertType) ? elementType : null, compare, _trackChanges, autoOff);
    }

    @Override
    public Date copyDate(Date orig) {
        if (orig == null)
            return null;
        if (orig instanceof Proxy)
            return (Date) ((Proxy) orig).copy(orig);

        ProxyDate proxy = getFactoryProxyDate(orig.getClass());
        return (Date) proxy.copy(orig);
    }

    @Override
    public Proxy newDateProxy(Class type) {
        ProxyDate proxy = getFactoryProxyDate(type);
        return proxy.newInstance();
    }

    @Override
    public Calendar copyCalendar(Calendar orig) {
        if (orig == null)
            return null;
        if (orig instanceof Proxy)
            return (Calendar) ((Proxy) orig).copy(orig);

        ProxyCalendar proxy = getFactoryProxyCalendar(orig.getClass());
        return (Calendar) proxy.copy(orig);
    }

    @Override
    public Proxy newCalendarProxy(Class type, TimeZone zone) {
        if (type == Calendar.class)
            type = GregorianCalendar.class;
        ProxyCalendar proxy = getFactoryProxyCalendar(type);
        ProxyCalendar cal = proxy.newInstance();
        if (zone != null)
            ((Calendar) cal).setTimeZone(zone);
        return cal;
    }

    @Override
    public Object copyCustom(Object orig) {
        if (orig == null)
            return null;
        if (orig instanceof Proxy)
            return ((Proxy) orig).copy(orig);
        if (ImplHelper.isManageable(orig))
            return null;
        if (orig instanceof Collection)
            return copyCollection((Collection) orig);
        if (orig instanceof Map)
            return copyMap((Map) orig);
        if (orig instanceof Date)
            return copyDate((Date) orig);
        if (orig instanceof Calendar)
            return copyCalendar((Calendar) orig);
        ProxyBean proxy = getFactoryProxyBean(orig);
        return (proxy == null) ? null : proxy.copy(orig);
    }

    @Override
    public Proxy newCustomProxy(Object orig, boolean autoOff) {
        if (orig == null)
            return null;
        if (orig instanceof Proxy)
            return (Proxy) orig;
        if (ImplHelper.isManageable(orig))
            return null;
        if (!isProxyable(orig.getClass()))
            return null;

        if (orig instanceof Collection) {
            Comparator comp = (orig instanceof SortedSet)
                ? ((SortedSet) orig).comparator() : null;
            Collection c = (Collection) newCollectionProxy(orig.getClass(),
                null, comp, autoOff);
            c.addAll((Collection) orig);
            return (Proxy) c;
        }
        if (orig instanceof Map) {
            Comparator comp = (orig instanceof SortedMap)
                ? ((SortedMap) orig).comparator() : null;
            Map m = (Map) newMapProxy(orig.getClass(), null, null, comp, autoOff);
            m.putAll((Map) orig);
            return (Proxy) m;
        }
        if (orig instanceof Date) {
            Date d = (Date) newDateProxy(orig.getClass());
            d.setTime(((Date) orig).getTime());
            if (orig instanceof Timestamp)
                ((Timestamp) d).setNanos(((Timestamp) orig).getNanos());
            return (Proxy) d;
        }
        if (orig instanceof Calendar) {
            Calendar c = (Calendar) newCalendarProxy(orig.getClass(),
                ((Calendar) orig).getTimeZone());
            c.setTimeInMillis(((Calendar) orig).getTimeInMillis());
            return (Proxy) c;
        }

        ProxyBean proxy = getFactoryProxyBean(orig);
        return (proxy == null) ? null : proxy.newInstance(orig);
    }

    /**
     * Return the concrete type for proxying.
     */
    protected Class toProxyableCollectionType(Class type) {
        if (type.getName().endsWith(PROXY_SUFFIX))
            type = type.getSuperclass();
        else if (type.isInterface()) {
            type = toConcreteType(type, _stdCollections);
            if (type == null)
                throw new UnsupportedException(_loc.get("no-proxy-intf", type));
        } else if (Modifier.isAbstract(type.getModifiers()))
            throw new UnsupportedException(_loc.get("no-proxy-abstract", type));
        return type;
    }

    /**
     * Return the concrete type for proxying.
     */
    protected Class toProxyableMapType(Class type) {
        if (type.getName().endsWith(PROXY_SUFFIX))
            type = type.getSuperclass();
        else if (type.isInterface()) {
            type = toConcreteType(type, _stdMaps);
            if (type == null)
                throw new UnsupportedException(_loc.get("no-proxy-intf", type));
        } else if (Modifier.isAbstract(type.getModifiers()))
            throw new UnsupportedException(_loc.get("no-proxy-abstract", type));
        return type;
    }

    /**
     * Locate a concrete type to proxy for the given collection interface.
     */
    private static Class toConcreteType(Class intf, Map concretes) {
        Class concrete = (Class) concretes.get(intf);
        if (concrete != null)
            return concrete;
        Class[] intfs = intf.getInterfaces();
        for (Class aClass : intfs) {
            concrete = toConcreteType(aClass, concretes);
            if (concrete != null)
                return concrete;
        }
        return null;
    }

    /**
     * Return the cached factory proxy for the given map type.
     */
    private ProxyMap getFactoryProxyMap(Class type) {
        // we don't lock here; ok if two proxies get generated for same type
        ProxyMap proxy = (ProxyMap) _proxies.get(type);
        if (proxy == null) {
            ClassLoader l = GeneratedClasses.getMostDerivedLoader(type,
                ProxyMap.class);
            Class pcls = loadBuildTimeProxy(type, l);
            if (pcls == null)
                pcls = generateAndLoadProxyMap(type, true, l);
            proxy = (ProxyMap) instantiateProxy(pcls, null, null);
            _proxies.put(type, proxy);
        }
        return proxy;
    }

    /**
     * Return the cached factory proxy for the given date type.
     */
    private ProxyDate getFactoryProxyDate(Class type) {
        // we don't lock here; ok if two proxies get generated for same type
        ProxyDate proxy = (ProxyDate) _proxies.get(type);
        if (proxy == null) {
            ClassLoader l = GeneratedClasses.getMostDerivedLoader(type,
                ProxyDate.class);
            Class pcls = loadBuildTimeProxy(type, l);
            if (pcls == null)
                pcls = generateAndLoadProxyDate(type, true, l);
            proxy = (ProxyDate) instantiateProxy(pcls, null, null);
            _proxies.put(type, proxy);
        }
        return proxy;
    }

    /**
     * Return the cached factory proxy for the given calendar type.
     */
    private ProxyCalendar getFactoryProxyCalendar(Class type) {
        // we don't lock here; ok if two proxies get generated for same type
        ProxyCalendar proxy = (ProxyCalendar) _proxies.get(type);
        if (proxy == null) {
            ClassLoader l = GeneratedClasses.getMostDerivedLoader(type,
                ProxyCalendar.class);
            Class pcls = loadBuildTimeProxy(type, l);
            if (pcls == null)
                pcls = generateAndLoadProxyCalendar(type, true, l);
            proxy = (ProxyCalendar) instantiateProxy(pcls, null, null);
            _proxies.put(type, proxy);
        }
        return proxy;
    }

    /**
     * Return the cached factory proxy for the given collection type.
     */
    private ProxyCollection getFactoryProxyCollection(Class type) {
        // we don't lock here; ok if two proxies get generated for same type
        ProxyCollection proxy = (ProxyCollection) _proxies.get(type);
        if (proxy == null) {
            ClassLoader l = GeneratedClasses.getMostDerivedLoader(type,
                    ProxyCollection.class);
            Class pcls = loadBuildTimeProxy(type, l);
            if (pcls == null)
                pcls = generateAndLoadProxyCollection(type, true, l);
            proxy = (ProxyCollection) instantiateProxy(pcls, null, null);
            _proxies.put(type, proxy);
        }
        return proxy;
    }


    /**
     * Return the cached factory proxy for the given bean type.
     */
    private ProxyBean getFactoryProxyBean(Object orig) {
        final Class<?> type = orig.getClass();
        if (isUnproxyable(type))
            return null;

        // we don't lock here; ok if two proxies get generated for same type
        ProxyBean proxy = (ProxyBean) _proxies.get(type);
        if (proxy == null) {
            ClassLoader l = GeneratedClasses.getMostDerivedLoader(type, ProxyBean.class);
            Class<?> pcls = loadBuildTimeProxy(type, l);
            if (pcls == null) {
                pcls = generateAndLoadProxyBean(type, true, l);
            }
            if (pcls != null)
                proxy = (ProxyBean) instantiateProxy(pcls, findCopyConstructor(type), new Object[] { orig });
            if (proxy == null) {
                _unproxyable.add(type.getName());
            } else {
                _proxies.put(type, proxy);
            }
        }
        return proxy;
    }

    /**
     * Return whether the given type is known to be unproxyable.
     */
    protected boolean isUnproxyable(Class type) {
        for (; type != null && type != Object.class;
            type = type.getSuperclass()) {
            if (_unproxyable.contains(type.getName()))
                return true;
        }
        return false;
    }

    /**
     * Load the proxy class generated at build time for the given type,
     * returning null if none exists.
     */
    protected Class loadBuildTimeProxy(Class type, ClassLoader loader) {
        try {
            Class<?> proxyClass = null;
            if (_delayedCollectionLoading) {
                proxyClass = loadDelayedProxy(type);
                if (proxyClass != null) {
                    return proxyClass;
                }
            }
            return Class.forName(getProxyClassName(type, false), true, loader);
        } catch (Throwable t) {
            return null;
        }
    }

    protected Class<?> loadDelayedProxy(Class<?> type) {
        if (type.equals(java.util.ArrayList.class)) {
            return DelayedArrayListProxy.class;
        }
        if (type.equals(java.util.HashSet.class)) {
            return DelayedHashSetProxy.class;
        }
        if (type.equals(java.util.LinkedList.class)) {
            return DelayedLinkedListProxy.class;
        }
        if (type.equals(java.util.Vector.class)) {
            return DelayedVectorProxy.class;
        }
        if (type.equals(java.util.LinkedHashSet.class)) {
            return DelayedLinkedHashSetProxy.class;
        }
        if (type.equals(java.util.SortedSet.class) || type.equals(java.util.TreeSet.class)) {
            return DelayedTreeSetProxy.class;
        }
        if (type.equals(java.util.PriorityQueue.class)) {
            return DelayedPriorityQueueProxy.class;
        }
        return null;
    }

    /**
     * Instantiate the given proxy class.
     */
    private Proxy instantiateProxy(Class cls, Constructor cons, Object[] args) {
        try {
            if (cons != null)
                return (Proxy) cls.getConstructor(cons.getParameterTypes()).
                    newInstance(args);
            return (Proxy) AccessController.doPrivileged(
                J2DoPrivHelper.newInstanceAction(cls));
        } catch (InstantiationException ie) {
            throw new UnsupportedException(_loc.get("cant-newinstance",
                cls.getSuperclass().getName()));
        } catch (PrivilegedActionException pae) {
            Exception e = pae.getException();
            if (e instanceof InstantiationException)
                throw new UnsupportedException(_loc.get("cant-newinstance",
                    cls.getSuperclass().getName()));
            else
                throw new GeneralException(cls.getName()).setCause(e);
        } catch (Throwable t) {
            throw new GeneralException(cls.getName()).setCause(t);
        }
    }

    /**
     * Return the name of the proxy class to generate for the given type.
     */
    protected static String getProxyClassName(Class type, boolean runtime) {
        String id = (runtime) ? "$" + nextProxyId() : "";
        return ClassUtil.getPackageName(ProxyManagerImpl.class) + "."
            + type.getName().replace('.', '$') + id + PROXY_SUFFIX;
    }

    /**
     * Throw appropriate exception if the given type is final.
     */
    private static void assertNotFinal(Class type) {
        if (Modifier.isFinal(type.getModifiers()))
            throw new UnsupportedException(_loc.get("no-proxy-final", type));
    }

    private static boolean isProxyable(Class<?> cls){
        int mod = cls.getModifiers();
        if(Modifier.isFinal(mod)) {
            return false;
        }

        if(Modifier.isProtected(mod) || Modifier.isPublic(mod)) {
            return true;
        }

        // Default scoped class, we can only extend if it is in the same package as the generated proxy. Ideally
        // we'd fix the code gen portion and place proxies in the same pacakge as the types being proxied.
        if(cls.getPackage().getName().equals("org.apache.openjpa.util")) {
            return true;
        }

        return false;

    }


    private Class generateAndLoadProxyDate(Class type, boolean runtime, ClassLoader l) {
        final String proxyClassName = getProxyClassName(type, runtime);
        final byte[] classBytes = generateProxyDateBytecode(type, runtime, proxyClassName);
        if (classBytes == null) {
            return null;
        }

        return GeneratedClasses.loadAsmClass(proxyClassName, classBytes, ProxyDate.class, l);
    }

    private Class generateAndLoadProxyCalendar(Class type, boolean runtime, ClassLoader l) {
        final String proxyClassName = getProxyClassName(type, runtime);
        final byte[] classBytes = generateProxyCalendarBytecode(type, runtime, proxyClassName);
        if (classBytes == null) {
            return null;
        }

        return GeneratedClasses.loadAsmClass(proxyClassName, classBytes, ProxyDate.class, l);
    }

    private Class generateAndLoadProxyCollection(Class type, boolean runtime, ClassLoader l) {
        final String proxyClassName = getProxyClassName(type, runtime);
        final byte[] classBytes = generateProxyCollectionBytecode(type, runtime, proxyClassName);
        if (classBytes == null) {
            return null;
        }

        return GeneratedClasses.loadAsmClass(proxyClassName, classBytes, ProxyCollection.class, l);
    }

    private Class generateAndLoadProxyMap(Class type, boolean runtime, ClassLoader l) {
        final String proxyClassName = getProxyClassName(type, runtime);
        final byte[] classBytes = generateProxyMapBytecode(type, runtime, proxyClassName);
        if (classBytes == null) {
            return null;
        }

        return GeneratedClasses.loadAsmClass(proxyClassName, classBytes, ProxyMap.class, l);
    }

    private Class generateAndLoadProxyBean(Class type, boolean runtime, ClassLoader l) {
        final String proxyClassName = getProxyClassName(type, runtime);
        final byte[] classBytes = generateProxyBeanBytecode(type, runtime, proxyClassName);
        if (classBytes == null) {
            return null;
        }

        return GeneratedClasses.loadAsmClass(proxyClassName, classBytes, ProxyBean.class, l);
    }

    /**
     * Generate the bytecode for a date proxy for the given type.
     */
    protected byte[] generateProxyDateBytecode(Class type, boolean runtime, String proxyClassName) {
        assertNotFinal(type);
        String proxyClassDef = proxyClassName.replace('.', '/');
        String superClassFileNname = Type.getInternalName(type);
        String[] interfaceNames = new String[]{Type.getInternalName(ProxyDate.class)};

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, proxyClassDef,
                null, superClassFileNname, interfaceNames);

        ClassWriterTracker ct = new ClassWriterTracker(cw);
        String classFileName = runtime ? type.getName() : proxyClassDef;
        cw.visitSource(classFileName + ".java", null);

        delegateConstructors(ct, type, superClassFileNname);
        addInstanceVariables(ct);
        addProxyMethods(ct, true, proxyClassDef, type);
        addProxyDateMethods(ct, proxyClassDef, type);
        proxySetters(ct, proxyClassDef, type);
        addWriteReplaceMethod(ct, proxyClassDef, runtime);

        return cw.toByteArray();
    }

    /**
     * Generate the bytecode for a calendar proxy for the given type.
     */
    protected byte[] generateProxyCalendarBytecode(Class type, boolean runtime, String proxyClassName) {
        assertNotFinal(type);
        String proxyClassDef = proxyClassName.replace('.', '/');
        String superClassFileNname = Type.getInternalName(type);
        String[] interfaceNames = new String[]{Type.getInternalName(ProxyCalendar.class)};

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, proxyClassDef,
                null, superClassFileNname, interfaceNames);

        ClassWriterTracker ct = new ClassWriterTracker(cw);
        String classFileName = runtime ? type.getName() : proxyClassDef;
        cw.visitSource(classFileName + ".java", null);

        delegateConstructors(ct, type, superClassFileNname);
        addInstanceVariables(ct);
        addProxyMethods(ct, true, proxyClassDef, type);
        addProxyCalendarMethods(ct, proxyClassDef, type);
        proxySetters(ct, proxyClassDef, type);
        addWriteReplaceMethod(ct, proxyClassDef, runtime);

        return cw.toByteArray();
    }

    /**
     * Generate the bytecode for a collection proxy for the given type.
     */
    protected byte[] generateProxyCollectionBytecode(Class type, boolean runtime, String proxyClassName) {
        assertNotFinal(type);
        String proxyClassDef = proxyClassName.replace('.', '/');
        String superClassFileNname = Type.getInternalName(type);
        String[] interfaceNames = new String[]{Type.getInternalName(ProxyCollection.class)};

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, proxyClassDef,
                null, superClassFileNname, interfaceNames);

        ClassWriterTracker ct = new ClassWriterTracker(cw);
        String classFileName = runtime ? type.getName() : proxyClassDef;
        cw.visitSource(classFileName + ".java", null);

        delegateConstructors(ct, type, superClassFileNname);
        addInstanceVariables(ct);
        addProxyMethods(ct, false, proxyClassDef, type);
        addProxyCollectionMethods(ct, proxyClassDef, type);
        proxyRecognizedMethods(ct, proxyClassDef, type, ProxyCollections.class, ProxyCollection.class);
        proxySetters(ct, proxyClassDef, type);
        addWriteReplaceMethod(ct, proxyClassDef, runtime);

        return cw.toByteArray();
    }

    /**
     * Generate the bytecode for a map proxy for the given type.
     */
    protected byte[] generateProxyMapBytecode(Class type, boolean runtime, String proxyClassName) {
        assertNotFinal(type);
        String proxyClassDef = proxyClassName.replace('.', '/');
        String superClassFileNname = Type.getInternalName(type);
        String[] interfaceNames = new String[]{Type.getInternalName(ProxyMap.class)};

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, proxyClassDef,
                null, superClassFileNname, interfaceNames);

        ClassWriterTracker ct = new ClassWriterTracker(cw);
        String classFileName = runtime ? type.getName() : proxyClassDef;
        cw.visitSource(classFileName + ".java", null);

        delegateConstructors(ct, type, superClassFileNname);
        addInstanceVariables(ct);
        addProxyMethods(ct, false, proxyClassDef, type);
        addProxyMapMethods(ct, proxyClassDef, type);
        proxyRecognizedMethods(ct, proxyClassDef, type, ProxyMaps.class, ProxyMap.class);
        proxySetters(ct, proxyClassDef, type);
        addWriteReplaceMethod(ct, proxyClassDef, runtime);

        return cw.toByteArray();
    }

    /**
     * Generate the bytecode for a bean proxy for the given type.
     */
    protected byte[] generateProxyBeanBytecode(Class type, boolean runtime, String proxyClassName) {
        if (Modifier.isFinal(type.getModifiers())) {
            return null;
        }
        if (ImplHelper.isManagedType(null, type)) {
            return null;
        }

        // we can only generate a valid proxy if there is a copy constructor
        // or a default constructor
        Constructor cons = findCopyConstructor(type);
        if (cons == null) {
            Constructor[] cs = type.getConstructors();
            for (int i = 0; cons == null && i < cs.length; i++) {
                if (cs[i].getParameterTypes().length == 0) {
                    cons = cs[i];
                }
            }
            if (cons == null)
                return null;
        }

        String proxyClassDef = proxyClassName.replace('.', '/');
        String superClassFileNname = Type.getInternalName(type);
        String[] interfaceNames = new String[]{Type.getInternalName(ProxyBean.class)};

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, proxyClassDef,
                null, superClassFileNname, interfaceNames);

        ClassWriterTracker ct = new ClassWriterTracker(cw);
        String classFileName = runtime ? type.getName() : proxyClassDef;
        cw.visitSource(classFileName + ".java", null);

        delegateConstructors(ct, type, superClassFileNname);
        addInstanceVariables(ct);
        addProxyMethods(ct, true, proxyClassDef, type);
        addProxyBeanMethods(ct, proxyClassDef, type, cons);
        if (!proxySetters(ct, proxyClassDef, type)) {
            return null;
        }
        addWriteReplaceMethod(ct, proxyClassDef, runtime);

        return cw.toByteArray();
    }

    private void addProxyBeanMethods(ClassWriterTracker ct, String proxyClassDef, Class type, Constructor cons) {
        // bean copy
        {
            MethodVisitor mv = ct.visitMethod(Modifier.PUBLIC, "copy",
                    Type.getMethodDescriptor(TYPE_OBJECT, TYPE_OBJECT)
                    , null, null);
            mv.visitCode();
            mv.visitTypeInsn(Opcodes.NEW, Type.getInternalName(type));
            mv.visitInsn(Opcodes.DUP);

            Class[] params = cons.getParameterTypes();
            if (params.length == 1) {
                mv.visitVarInsn(Opcodes.ALOAD, 1);
                mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(params[0]));
            }
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(type), "<init>",
                    Type.getMethodDescriptor(Type.VOID_TYPE, AsmHelper.getParamTypes(params)), false);
            int beanVarPos = params.length+2; // params+DUP

            if (params.length == 0) {
                mv.visitVarInsn(Opcodes.ASTORE, beanVarPos);
                copyBeanProperties(mv, type, beanVarPos);
                mv.visitVarInsn(Opcodes.ALOAD, beanVarPos);
            }

            mv.visitInsn(Opcodes.ARETURN);
            mv.visitMaxs(-1, -1);
            mv.visitEnd();
        }

        // new instance factory
        {
            MethodVisitor mv = ct.visitMethod(Modifier.PUBLIC, "newInstance",
                    Type.getMethodDescriptor(Type.getType(ProxyBean.class), Type.getType(Object.class))
                    , null, null);
