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

import java.util.BitSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.AbstractPCData;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.PCData;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.asm.AsmHelper;
import org.apache.openjpa.util.asm.ClassNodeTracker;
import org.apache.xbean.asm9.Opcodes;
import org.apache.xbean.asm9.Type;
import org.apache.xbean.asm9.tree.ClassNode;
import org.apache.xbean.asm9.tree.FieldInsnNode;
import org.apache.xbean.asm9.tree.FieldNode;
import org.apache.xbean.asm9.tree.InsnList;
import org.apache.xbean.asm9.tree.InsnNode;
import org.apache.xbean.asm9.tree.JumpInsnNode;
import org.apache.xbean.asm9.tree.LabelNode;
import org.apache.xbean.asm9.tree.LookupSwitchInsnNode;
import org.apache.xbean.asm9.tree.MethodInsnNode;
import org.apache.xbean.asm9.tree.MethodNode;
import org.apache.xbean.asm9.tree.TypeInsnNode;
import org.apache.xbean.asm9.tree.VarInsnNode;


/**
 * Generates {@link PCData} instances which avoid primitive wrappers
 * to optimize memory use and performance at the cost of slightly higher
 * startup time.
 *
 * @author Steve Kim
 * @author Mark Struberg rework to ASM
 * @since 0.3.2
 */
public class PCDataGenerator extends DynamicStorageGenerator {

    private static final Localizer _loc = Localizer.forPackage(PCDataGenerator.class);

    protected static final String POSTFIX = "$openjpapcdata";

    private final Map<Class<?>, DynamicStorage> _generated = new ConcurrentHashMap<>();
    private final OpenJPAConfiguration _conf;
    private final Log _log;

    public PCDataGenerator(OpenJPAConfiguration conf) {
        _conf = conf;
        _log = _conf.getLogFactory().getLog(OpenJPAConfiguration.LOG_ENHANCE);
    }

    /**
     * Return the configuration.
     */
    public OpenJPAConfiguration getConfiguration() {
        return _conf;
    }

    /**
     * Return a {@link PCData} instance for the given oid and metadata.
     */
    public PCData generatePCData(Object oid, ClassMetaData meta) {
        if (meta == null) {
            return null;
        }

        Class<?> type = meta.getDescribedType();
        DynamicStorage storage = _generated.get(type);
        if (storage == null) {
            storage = generateStorage(meta);
            _generated.put(type, storage);
            if (_log.isTraceEnabled())
                _log.trace(_loc.get("pcdata-created", type.getName(), meta));
        }
        DynamicPCData data = (DynamicPCData) storage.newInstance();
        data.setId(oid);
        data.setStorageGenerator(this);
        finish(data, meta);
        return data;
    }

    /**
     * Actually generate the factory instance.
     */
    private DynamicStorage generateStorage(ClassMetaData meta) {
        if (_log.isTraceEnabled()) {
            _log.trace(_loc.get("pcdata-generate", meta));
        }

        FieldMetaData[] fields = meta.getFields();
        int[] types = new int[fields.length];
        for (int i = 0; i < types.length; i++) {
            types[i] = replaceType(fields[i]);
        }
        return generateStorage(types, meta);
    }

    /**
     * Perform any final actions before the pcdata is returned to client code.
     */
    protected void finish(DynamicPCData data, ClassMetaData meta) {
    }

    @Override
    protected int getCreateFieldMethods(int typeCode) {
        if (typeCode >= JavaTypes.OBJECT) {
            return POLICY_SILENT;
        }

        // don't bother creating set/get<Primitive> methods
        return POLICY_EMPTY;
    }

    @Override
    protected void declareClasses(ClassNodeTracker bc) {
        super.declareClasses(bc);
        bc.declareInterface(DynamicPCData.class);
        bc.getClassNode().superName = Type.getInternalName(AbstractPCData.class);
    }

    @Override
    protected final String getClassName(Object obj) {
        return getUniqueName(((ClassMetaData) obj).getDescribedType());
    }

    /**
     * Creates a unique name for the given type's pcdata implementation.
     */
    protected String getUniqueName(Class<?> type) {
        return type.getName() + "$" + System.identityHashCode(type) + POSTFIX;
    }

    @Override
    protected final void decorate(Object obj, ClassNodeTracker bc, int[] types) {
        super.decorate(obj, bc, types);
        ClassMetaData meta = (ClassMetaData) obj;

        enhanceConstructor(bc);
        addBaseFields(bc);
        addImplDataMethods(bc, meta);
        addGetType(bc, meta);
        addVersionMethods(bc);
        addFieldImplDataMethods(bc, meta);
        addLoadMethod(bc, meta);
        addLoadWithFieldsMethod(bc, meta);
        addStoreMethods(bc, meta);
        addNewEmbedded(bc);
        addGetData(bc);

        decorate(bc, meta);
    }

    /**
     * Apply additional decoration to generated class.
     */
    protected void decorate(ClassNodeTracker bc, ClassMetaData meta) {
    }

    /**
     * Enhance constructor to initialize fields
     */
    private void enhanceConstructor(ClassNodeTracker bc) {
        ClassNode classNode = bc.getClassNode();

        // find the default constructor
        MethodNode defaultCt = classNode.methods.stream()
                .filter(m -> m.name.equals("<init>") && m.desc.equals("()V"))
                .findFirst()
                .get();

        InsnList instructions = new InsnList();


        // private BitSet loaded = new BitSet();
        FieldNode loaded = addBeanField(bc, "loaded", BitSet.class);

        instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
        instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(BitSet.class)));
        instructions.add(new InsnNode(Opcodes.DUP));
        instructions.add(AsmHelper.getLoadConstantInsn(classNode.fields.size()));
        instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL,
                                            Type.getInternalName(BitSet.class),
                                            "<init>",
                                            Type.getMethodDescriptor(Type.VOID_TYPE, Type.INT_TYPE)));
        instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, loaded.name, loaded.desc));

        defaultCt.instructions.insertBefore(defaultCt.instructions.getLast(), instructions);
    }

    /**
     * Have to load the type since it may not be available to the
     * same classloader (i.e. rar vs. ear). The context classloader
     * (i.e. the user app classloader) should be fine.
     */
    private void addGetType(ClassNodeTracker bc, ClassMetaData meta) {
        ClassNode classNode = bc.getClassNode();
        FieldNode typeField = new FieldNode(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, "type", Type.getDescriptor(Class.class), null, null);
        classNode.fields.add(typeField);

        // public Class getType() {
        MethodNode getter = new MethodNode(Opcodes.ACC_PUBLIC,
                                           "getType",
                                           Type.getMethodDescriptor(Type.getType(Class.class)),
                                           null, null);
        classNode.methods.add(getter);

        InsnList instructions = getter.instructions;

        // use name as constant filled with meta.getDescribedType().getName()
        // if (type == null) {
        //     type = PCDataGenerator.getType(name)
        // }
        instructions.add(new FieldInsnNode(Opcodes.GETSTATIC, classNode.name, typeField.name, typeField.desc));

        LabelNode lblEndIfNN = new LabelNode();
        instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIfNN));

        // actual type = PCDataGenerator.getType(name)
        instructions.add(AsmHelper.getLoadConstantInsn(meta.getDescribedType().getName()));
        instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
                                            Type.getInternalName(PCDataGenerator.class),
                                            "getType",
                                            Type.getMethodDescriptor(Type.getType(Class.class), Type.getType(String.class))));
        instructions.add(new FieldInsnNode(Opcodes.PUTSTATIC, classNode.name, typeField.name, typeField.desc));

        instructions.add(lblEndIfNN);
        instructions.add(new FieldInsnNode(Opcodes.GETSTATIC, classNode.name, typeField.name, typeField.desc));
        instructions.add(new InsnNode(Opcodes.ARETURN));
    }

    public static Class<?> getType(String className) {
        try {
            return Class.forName(className, true, Thread.currentThread().getContextClassLoader());
        }
        catch (ClassNotFoundException cnfe) {
            throw new InternalException();
        }
    }

    /**
     * Declare standard dynamic pcdata fields.
     */
    private void addBaseFields(ClassNodeTracker bc) {
        addBeanField(bc, "id", Object.class);
        FieldNode field = addBeanField(bc, "storageGenerator", PCDataGenerator.class);
        field.access |= Opcodes.ACC_TRANSIENT;
    }

    /**
     * Add methods for loading and storing class-level impl data.
     */
    private void addImplDataMethods(ClassNodeTracker bc, ClassMetaData meta) {
        ClassNode classNode = bc.getClassNode();

        // void storeImplData(OpenJPAStateManager);
        MethodNode storeM = new MethodNode(Opcodes.ACC_PUBLIC,
                                         "storeImplData",
                                         Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(OpenJPAStateManager.class)),
                                         null, null);
        classNode.methods.add(storeM);
        InsnList instructions = storeM.instructions;

        FieldNode impl = null;
        if (!usesImplData(meta)) {
            instructions.add(new InsnNode(Opcodes.RETURN));
        }
        else {
            // if (sm.isImplDataCacheable())
            //         setImplData(sm.getImplData());
            impl = addBeanField(bc, "implData", Object.class);
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "isImplDataCacheable",
                                                Type.getMethodDescriptor(Type.BOOLEAN_TYPE)));
            LabelNode lblEndIfEq = new LabelNode();
            instructions.add(new JumpInsnNode(Opcodes.IFEQ, lblEndIfEq));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "getImplData",
                                                Type.getMethodDescriptor(AsmHelper.TYPE_OBJECT)));

            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
                                                classNode.name,
                                                "setImplData",
                                                Type.getMethodDescriptor(Type.VOID_TYPE, AsmHelper.TYPE_OBJECT)));

            instructions.add(lblEndIfEq);
            instructions.add(new InsnNode(Opcodes.RETURN));
        }

        // void loadImplData(OpenJPAStateManager);
        MethodNode loadM = new MethodNode(Opcodes.ACC_PUBLIC,
                                           "loadImplData",
                                           Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(OpenJPAStateManager.class)),
                                           null, null);
        classNode.methods.add(loadM);
        instructions = loadM.instructions;

        if (!usesImplData(meta)) {
            instructions.add(new InsnNode(Opcodes.RETURN));
        }
        else {
            // if (sm.getImplData() == null && implData != null)
            //         sm.setImplData(impl, true);
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "getImplData",
                                                Type.getMethodDescriptor(AsmHelper.TYPE_OBJECT)));
            LabelNode lblEndIf = new LabelNode();
            instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIf));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
            instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));

            LabelNode lblEndIf2 = new LabelNode();
            instructions.add(new JumpInsnNode(Opcodes.IFNULL, lblEndIf2));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
            instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));
            instructions.add(AsmHelper.getLoadConstantInsn(true));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "setImplData",
                                                Type.getMethodDescriptor(Type.VOID_TYPE, AsmHelper.TYPE_OBJECT, Type.BOOLEAN_TYPE)));
            instructions.add(lblEndIf);
            instructions.add(lblEndIf2);
            instructions.add(new InsnNode(Opcodes.RETURN));
        }
    }

    /**
     * Add methods for loading and storing class-level impl data.
     */
    private void addFieldImplDataMethods(ClassNodeTracker cnt, ClassMetaData meta) {
        ClassNode classNode = cnt.getClassNode();
        int count = countImplDataFields(meta);
        FieldNode impl = null;

        // public void loadImplData(OpenJPAStateManager sm, int i)
        {
            MethodNode meth = new MethodNode(Opcodes.ACC_PRIVATE,
                                             "loadImplData",
                                             Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(OpenJPAStateManager.class), Type.INT_TYPE),
                                             null, null);
            classNode.methods.add(meth);
            InsnList instructions = meth.instructions;

            if (count == 0) {
                instructions.add(new InsnNode(Opcodes.RETURN));
            }
            else {
                // Object[] fieldImpl
                impl = new FieldNode(Opcodes.ACC_PRIVATE, "fieldImpl", Type.getDescriptor(Object[].class), null, null);
                classNode.fields.add(impl);

                // if (fieldImpl != null)
                instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
                instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));

                LabelNode lblEndIf = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIf));
                instructions.add(new InsnNode(Opcodes.RETURN));
                instructions.add(lblEndIf);

                // Object obj = null;
                int objVarPos = AsmHelper.getLocalVarPos(meth);
                instructions.add(new InsnNode(Opcodes.ACONST_NULL));
                instructions.add(new VarInsnNode(Opcodes.ASTORE, objVarPos));

                LabelNode lblEnd = new LabelNode();
                instructions.add(new VarInsnNode(Opcodes.ILOAD, 2));
                // switch(i)
                LookupSwitchInsnNode lSwitch = new LookupSwitchInsnNode(lblEnd, null, null);
                FieldMetaData[] fields = meta.getFields();
                int cacheable = 0;
                for (int i = 0; i < fields.length; i++) {
                    if (!usesImplData(fields[i])) {
                        continue;
                    }

                    // case x: obj = fieldImpl[y]; break;
                    LabelNode lblCase = new LabelNode();
                    instructions.add(lblCase);
                    lSwitch.keys.add(i);
                    lSwitch.labels.add(lblCase);
                    instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));
                    instructions.add(AsmHelper.getLoadConstantInsn(cacheable++));
                    instructions.add(new InsnNode(Opcodes.AALOAD));
                    instructions.add(new VarInsnNode(Opcodes.ASTORE, objVarPos));
                    instructions.add(new JumpInsnNode(Opcodes.GOTO, lblEnd));
                }
                // 'default:' is empty

                instructions.add(lblEnd);
                instructions.add(new VarInsnNode(Opcodes.ALOAD, objVarPos));

                // if (obj != null) return;
                lblEndIf = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIf));
                instructions.add(new InsnNode(Opcodes.RETURN));

                // end if
                instructions.add(lblEndIf);

                // sm.setImplData(index, impl);
                instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st parameter OpenJPAStateManager
                instructions.add(new VarInsnNode(Opcodes.ILOAD, 2)); // 2nd parameter int
                instructions.add(new VarInsnNode(Opcodes.ALOAD, objVarPos)); // the previously stored fieldImpl
                instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                    Type.getInternalName(OpenJPAStateManager.class),
                                                    "setImplData",
                                                    Type.getMethodDescriptor(Type.VOID_TYPE, Type.INT_TYPE, AsmHelper.TYPE_OBJECT)));
                instructions.add(new InsnNode(Opcodes.RETURN));
            }
        }

        // void storeImplData(OpenJPAStateManager sm, int index, boolean loaded)
        {
            MethodNode meth = new MethodNode(Opcodes.ACC_PRIVATE,
                                             "storeImplData",
                                             Type.getMethodDescriptor(Type.VOID_TYPE,
                                                                      Type.getType(OpenJPAStateManager.class), Type.INT_TYPE, Type.BOOLEAN_TYPE),
                                             null, null);
            classNode.methods.add(meth);
            InsnList instructions = meth.instructions;

            if (count == 0) {
                instructions.add(new InsnNode(Opcodes.RETURN));
            }
            else {
                // int arrIdx = -1;
                // switch(index)
                int arrIdxVarPos = AsmHelper.getLocalVarPos(meth);
                instructions.add(AsmHelper.getLoadConstantInsn(-1));
                instructions.add(new VarInsnNode(Opcodes.ISTORE, arrIdxVarPos));
                instructions.add(new VarInsnNode(Opcodes.ILOAD, 2)); // 2nd param int

                LabelNode lblEnd = new LabelNode();
                // switch(i)
                LookupSwitchInsnNode lSwitch = new LookupSwitchInsnNode(lblEnd, null, null);

                FieldMetaData[] fields = meta.getFields();
                int cacheable = 0;
                for (int i = 0; i < fields.length; i++) {
                    if (!usesImplData(fields[i])) {
                        continue;
                    }

                    // case x: arrIdx = y; break;
                    LabelNode lblCase = new LabelNode();
                    instructions.add(lblCase);
                    lSwitch.keys.add(i);
                    lSwitch.labels.add(lblCase);
                    instructions.add(AsmHelper.getLoadConstantInsn(cacheable++));
                    instructions.add(new VarInsnNode(Opcodes.ISTORE, arrIdxVarPos));
                    instructions.add(new JumpInsnNode(Opcodes.GOTO, lblEnd));
                }
                // 'default:' is empty

                instructions.add(lblEnd);

                // if (arrIdx != -1)
                instructions.add(AsmHelper.getLoadConstantInsn(-1));
                LabelNode lblEndIf = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.IF_ICMPNE, lblEndIf));
                instructions.add(new InsnNode(Opcodes.RETURN));

                // end if
                instructions.add(lblEndIf);

                // if (loaded)
                instructions.add(new VarInsnNode(Opcodes.ILOAD, 3)); // 3rd param, boolean
                lblEndIf = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.IFEQ, lblEndIf));

                // Object obj = sm.getImplData(index)
                int objVarPos = arrIdxVarPos+1;
                instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param, OpenJPAStateManager
                instructions.add(new VarInsnNode(Opcodes.ILOAD, 2)); // 2st param, int
                instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                    Type.getInternalName(OpenJPAStateManager.class),
                                                    "getImplData",
                                                    Type.getMethodDescriptor(AsmHelper.TYPE_OBJECT, Type.INT_TYPE)));
                instructions.add(new VarInsnNode(Opcodes.ASTORE, objVarPos));

                // if (fieldImpl == null)
                //     fieldImpl = new Object[fields];

                instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
                instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));

                LabelNode lblEndIfNN = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIfNN));
                instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
                instructions.add(AsmHelper.getLoadConstantInsn(count));
                instructions.add(new TypeInsnNode(Opcodes.ANEWARRAY, Type.getInternalName(Object.class)));
                instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, impl.name, impl.desc));
                instructions.add(lblEndIfNN);

                // fieldImpl[arrIdx] = obj;
                // return;
                instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
                instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));
                instructions.add(new VarInsnNode(Opcodes.ILOAD, arrIdxVarPos));
                instructions.add(new VarInsnNode(Opcodes.ALOAD, objVarPos));
                instructions.add(new InsnNode(Opcodes.AASTORE));
                instructions.add(new InsnNode(Opcodes.RETURN));

                instructions.add(lblEndIf);

                // if (fieldImpl != null)
                //         fieldImpl[index] = null;
                instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));
                lblEndIfNN = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIfNN));
                instructions.add(new InsnNode(Opcodes.RETURN));

                instructions.add(lblEndIfNN);
                instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
                instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, impl.name, impl.desc));
                instructions.add(new VarInsnNode(Opcodes.ILOAD, arrIdxVarPos));
                instructions.add(new InsnNode(Opcodes.ACONST_NULL));
                instructions.add(new InsnNode(Opcodes.AASTORE));
                instructions.add(new InsnNode(Opcodes.RETURN));
            }
        }
    }

    /**
     * Add methods for loading and storing version data.
     */
    protected void addVersionMethods(ClassNodeTracker bc) {
        ClassNode classNode = bc.getClassNode();

        final FieldNode versionField = addBeanField(bc, "version", Object.class);

        // void storeVersion(OpenJPAStateManager sm);
        {
            MethodNode storeMeth = new MethodNode(Opcodes.ACC_PUBLIC,
                                                  "storeVersion",
                                                  Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(OpenJPAStateManager.class)),
                                                  null, null);
            classNode.methods.add(storeMeth);
            InsnList instructions = storeMeth.instructions;


            // version = sm.getVersion();
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "getVersion",
                                                Type.getMethodDescriptor(AsmHelper.TYPE_OBJECT)));
            instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, versionField.name, versionField.desc));
            instructions.add(new InsnNode(Opcodes.RETURN));
        }

        // void loadVersion(OpenJPAStateManager sm)
        {
            MethodNode loadMeth = new MethodNode(Opcodes.ACC_PUBLIC,
                                                 "loadVersion",
                                                 Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(OpenJPAStateManager.class)),
                                                 null, null);
            classNode.methods.add(loadMeth);
            InsnList instructions = loadMeth.instructions;

            // if (sm.getVersion() == null)
            //         sm.setVersion(version);
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "getVersion",
                                                Type.getMethodDescriptor(AsmHelper.TYPE_OBJECT)));

            LabelNode lblEndIf = new LabelNode();
            instructions.add(new JumpInsnNode(Opcodes.IFNONNULL, lblEndIf));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
            instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, versionField.name, versionField.desc));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "setVersion",
                                                Type.getMethodDescriptor(Type.VOID_TYPE, AsmHelper.TYPE_OBJECT)));

            instructions.add(lblEndIf);
            instructions.add(new InsnNode(Opcodes.RETURN));
        }
    }

    private void addLoadMethod(ClassNodeTracker cnt, ClassMetaData meta) {
        final ClassNode classNode = cnt.getClassNode();

        // public void load(OpenJPAStateManager sm, FetchConfiguration fetch, Object context)
        MethodNode meth = addLoadMethod(cnt, false);
        InsnList instructions = meth.instructions;

        FieldMetaData[] fmds = meta.getFields();

        int localVarPos = AsmHelper.getLocalVarPos(meth);
        instructions.add(new InsnNode(Opcodes.ACONST_NULL));
        instructions.add(new VarInsnNode(Opcodes.ASTORE, localVarPos));
        int interVarPos = localVarPos + 1;
        instructions.add(new InsnNode(Opcodes.ACONST_NULL));
        instructions.add(new VarInsnNode(Opcodes.ASTORE, interVarPos));

        int objectCount = 0;
        boolean intermediate;
        LabelNode lblEndIf = null;
        for (int i = 0; i < fmds.length; i++) {
            if (lblEndIf != null) {
                instructions.add(lblEndIf);
            }
            lblEndIf = new LabelNode();

            intermediate = usesIntermediate(fmds[i]);

            instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this

            // if (loaded.get(i)) or (!loaded.get(i)) depending on inter resp
            instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, "loaded", Type.getDescriptor(BitSet.class)));
            instructions.add(AsmHelper.getLoadConstantInsn(i));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
                                                Type.getInternalName(BitSet.class),
                                                "get",
                                                Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.INT_TYPE)));
            instructions.add(new JumpInsnNode(intermediate ? Opcodes.IFNE : Opcodes.IFEQ, lblEndIf));

            // if (fetch.requiresFetch(fmds[i])!=FetchConfiguration.FETCH_NONE)

            if (intermediate) {
                addLoadIntermediate(classNode, instructions, i, objectCount, interVarPos);

                LabelNode lblElse = lblEndIf;
                lblEndIf = new LabelNode();
                instructions.add(new JumpInsnNode(Opcodes.GOTO, lblEndIf));
                instructions.add(lblElse);
            }

            instructions.add(new VarInsnNode(Opcodes.ALOAD, 2)); // 2nd param FetchConfiguration
            instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // 1st param OpenJPAStateManager
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(OpenJPAStateManager.class),
                                                "getMetaData",
                                                Type.getMethodDescriptor(Type.getType(ClassMetaData.class))));
            instructions.add(AsmHelper.getLoadConstantInsn(fmds[i].getIndex()));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
                                                Type.getInternalName(ClassMetaData.class),
                                                "getField",
                                                Type.getMethodDescriptor(Type.getType(FieldMetaData.class), Type.INT_TYPE)));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE,
                                                Type.getInternalName(FetchConfiguration.class),
                                                "requiresFetch",
                                                Type.getMethodDescriptor(Type.INT_TYPE, Type.getType(FieldMetaData.class))));
            instructions.add(AsmHelper.getLoadConstantInsn(FetchConfiguration.FETCH_NONE));
            instructions.add(new JumpInsnNode(Opcodes.IF_ICMPEQ, lblEndIf));
            addLoad(classNode, instructions, fmds[i], objectCount, localVarPos, false);


            if (replaceType(fmds[i]) >= JavaTypes.OBJECT) {
                objectCount++;
            }
        }

        if (lblEndIf != null) {
            instructions.add(lblEndIf);
        }

        instructions.add(new InsnNode(Opcodes.RETURN));
    }
