package org.raku.nqp.sixmodel.reprs;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.raku.nqp.runtime.ExceptionHandling;
import org.raku.nqp.runtime.Ops;
import org.raku.nqp.runtime.ThreadContext;
import org.raku.nqp.sixmodel.REPR;
import org.raku.nqp.sixmodel.STable;
import org.raku.nqp.sixmodel.SerializationReader;
import org.raku.nqp.sixmodel.SerializationWriter;
import org.raku.nqp.sixmodel.SixModelObject;
import org.raku.nqp.sixmodel.StorageSpec;
import org.raku.nqp.sixmodel.TypeObject;

public class P6Opaque extends REPR {
    private static long typeId = 0;

    private static class AttrInfo {
        public STable st;
        public boolean boxTarget;
        public boolean hasAutoVivContainer;
        public boolean posDelegate;
        public boolean assDelegate;
    }

    // this is a weak *value* map; it provides instances of V, but does not retain them, and keeps strong refs to the keys
    @SuppressWarnings("unchecked")
    private static class ObjectCache<K,V> {
        private class Ref extends SoftReference<V> {
            public K key;
            public Ref(K key, V obj, ReferenceQueue<V> queue) { super(obj, queue); this.key = key; }
        }

        private HashMap<K, Ref> store = new HashMap< >();
        private ReferenceQueue<V> queue = new ReferenceQueue< >();

        public synchronized V get(K key) {
            Ref ref;
            while ((ref = (Ref) queue.poll()) != null) store.remove(ref.key);
            ref = store.get(key);
            return ref == null ? null : ref.get();
        }

        public synchronized void put(K key, V value) {
            Ref ref;
            while ((ref = (Ref) queue.poll()) != null) store.remove(ref.key);
            store.put(key, new Ref(key, value, queue));
        }
    }

    private static ObjectCache<String, Class<?>> classCache = new ObjectCache< >();

    @Override
    public SixModelObject type_object_for(ThreadContext tc, SixModelObject HOW) {
        STable st = new STable(this, HOW);
        st.REPRData = new P6OpaqueREPRData();
        SixModelObject obj = new TypeObject();
        obj.st = st;
        st.WHAT = obj;
        return st.WHAT;
    }

    @Override
    @SuppressWarnings("unchecked") // Because Java implemented generics stupidly
    public void compose(ThreadContext tc, STable st, SixModelObject repr_info_hash) {
        /* Get attribute part of the protocol from the hash. */
        SixModelObject repr_info = repr_info_hash.at_key_boxed(tc, "attribute");

        /* Go through MRO and find all classes with attributes and build up
         * mapping info hashes. Note, reverse order so indexes will match
         * those in parent types. */
        int curAttr = 0;
        boolean mi = false;
        List<SixModelObject> classHandles = new ArrayList<SixModelObject>();
        ArrayList<Object2IntOpenHashMap<String>> attrIndexes = new ArrayList<Object2IntOpenHashMap<String>>();
        List<SixModelObject> autoVivs = new ArrayList<SixModelObject>();
        List<STable> flattenedSTables = new ArrayList<STable>();
        List<AttrInfo> attrInfoList = new ArrayList<AttrInfo>();
        long mroLength = repr_info.elems(tc);
        for (long i = mroLength - 1; i >= 0; i--) {
            SixModelObject entry = repr_info.at_pos_boxed(tc, i);
            SixModelObject type = entry.at_pos_boxed(tc, 0);
            SixModelObject attrs = entry.at_pos_boxed(tc, 1);
            SixModelObject parents = entry.at_pos_boxed(tc, 2);

            /* If it has any attributes, give them each indexes and put them
             * in the list to add to the layout. */
            long numAttrs = attrs.elems(tc);
            if (numAttrs > 0) {
                Object2IntOpenHashMap<String> indexes = new Object2IntOpenHashMap<String>();
                for (long j = 0; j < numAttrs; j++) {
                    SixModelObject attrHash = attrs.at_pos_boxed(tc, j);
                    String attrName = attrHash.at_key_boxed(tc, "name").get_str(tc);
                    SixModelObject attrType = attrHash.at_key_boxed(tc, "type");
                    if (Ops.isnull(attrType) == 1)
                        attrType = tc.gc.KnowHOW;
                    indexes.put(attrName, curAttr);
                    AttrInfo info = new AttrInfo();
                    info.st = attrType.st;
                    if (attrType.st.REPR.get_storage_spec(tc, attrType.st).inlineable == StorageSpec.INLINED)
                        flattenedSTables.add(attrType.st);
                    else
                        flattenedSTables.add(null);
                    info.boxTarget = attrHash.exists_key(tc, "box_target") != 0;
                    SixModelObject autoViv = attrHash.at_key_boxed(tc, "auto_viv_container");
                    autoVivs.add(autoViv);
                    if (Ops.isnull(autoViv) == 0)
                        info.hasAutoVivContainer = true;
                    info.posDelegate = attrHash.exists_key(tc, "positional_delegate") != 0;
                    info.assDelegate = attrHash.exists_key(tc, "associative_delegate") != 0;
                    attrInfoList.add(info);

                    if (info.boxTarget) {
                        switch (info.st.REPR.get_storage_spec(tc, info.st).boxed_primitive) {
                        case StorageSpec.BP_INT:
                            ((P6OpaqueREPRData)st.REPRData).unboxIntSlot = curAttr;
                            break;
                        //case StorageSpec.BP_UINT:
                        //    ((P6OpaqueREPRData)st.REPRData).unboxUIntSlot = curAttr;
                        //    break;
                        case StorageSpec.BP_NUM:
                            ((P6OpaqueREPRData)st.REPRData).unboxNumSlot = curAttr;
                            break;
                        case StorageSpec.BP_STR:
                            ((P6OpaqueREPRData)st.REPRData).unboxStrSlot = curAttr;
                            break;
                        default:
                            ((P6OpaqueREPRData)st.REPRData).unboxObjSlot = curAttr;
                            break;
                        }
                    }
                    if (info.posDelegate)
                        ((P6OpaqueREPRData)st.REPRData).posDelSlot = curAttr;
                    if (info.assDelegate)
                        ((P6OpaqueREPRData)st.REPRData).assDelSlot = curAttr;

                    curAttr++;
                }
                classHandles.add(type);
                attrIndexes.add(indexes);
            }

            /* Multiple parents means it's multiple inheritance. */
            if (parents.elems(tc) > 1)
                mi = true;
        }

        /* Populate some REPR data. */
        ((P6OpaqueREPRData)st.REPRData).classHandles = classHandles.toArray(new SixModelObject[0]);
        ((P6OpaqueREPRData)st.REPRData).nameToHintMap = attrIndexes;
        ((P6OpaqueREPRData)st.REPRData).autoVivContainers = autoVivs.toArray(new SixModelObject[0]);
        ((P6OpaqueREPRData)st.REPRData).flattenedSTables = flattenedSTables.toArray(new STable[0]);
        ((P6OpaqueREPRData)st.REPRData).mi = mi;

        /* Provided we have attributes, generate the JVM backing type. If not,
         * P6OpaqueBaseInstance will do. */
        if (attrInfoList.size() > 0) {
            installJVMType(tc, st, attrInfoList);
        }
        else {
            ((P6OpaqueREPRData)st.REPRData).jvmClass = P6OpaqueBaseInstance.class;
            ((P6OpaqueREPRData)st.REPRData).instance = new P6OpaqueBaseInstance();
            ((P6OpaqueREPRData)st.REPRData).instance.st = st;
        }
    }

    /* Adds delegation, needed for mixin support. */
    private void addDelegation(MethodVisitor mv, String methodName,
            Type retType, Type[] argTypes, boolean hasValue) {

        mv.visitVarInsn(Opcodes.ALOAD, 0); // this
        mv.visitFieldInsn(Opcodes.GETFIELD, "org/raku/nqp/sixmodel/reprs/P6OpaqueBaseInstance", "delegate",
                "Lorg/raku/nqp/sixmodel/SixModelObject;");
        mv.visitInsn(Opcodes.DUP);

        Label label = new Label();
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/raku/nqp/runtime/Ops", "isnull", "(Lorg/raku/nqp/sixmodel/SixModelObject;)J");
        mv.visitInsn(Opcodes.LCONST_1);
        mv.visitInsn(Opcodes.LCMP);
        mv.visitJumpInsn(Opcodes.IFEQ, label);

        mv.visitVarInsn(Opcodes.ALOAD, 1); // tc
        mv.visitVarInsn(Opcodes.ALOAD, 2); // class_handle
        mv.visitVarInsn(Opcodes.ALOAD, 3); // name
        mv.visitVarInsn(Opcodes.LLOAD, 4); // hint
        if (hasValue)
            mv.visitVarInsn(Opcodes.ALOAD, 6); // value

        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/raku/nqp/sixmodel/SixModelObject", methodName,
                Type.getMethodDescriptor(retType, argTypes));

        mv.visitInsn(retType == Type.VOID_TYPE ? Opcodes.RETURN :
                     retType == Type.LONG_TYPE ? Opcodes.LRETURN :
                                                 Opcodes.ARETURN);
        mv.visitLabel(label);
        mv.visitInsn(Opcodes.POP);
    }

    private void installJVMType(ThreadContext tc, STable st, List<AttrInfo> attrInfoList) {
        StringBuilder sigBuilder = new StringBuilder();

        boolean forceNew = false;
        for (AttrInfo attr : attrInfoList) {

            if (attr.hasAutoVivContainer) sigBuilder.append('v');
            if (attr.posDelegate) sigBuilder.append('p');
            if (attr.assDelegate) sigBuilder.append('a');

            StorageSpec ss = attr.st.REPR.get_storage_spec(tc, attr.st);
            if (ss.inlineable != StorageSpec.REFERENCE) {
                sigBuilder.append('I');
                sigBuilder.append(attr.st.REPR.name);
                sigBuilder.append('(');
                if (!attr.st.REPR.inline_description(tc, attr.st, sigBuilder)) forceNew = true;
                sigBuilder.append(')');
            }

            if (attr.boxTarget) {
                sigBuilder.append('B');
                sigBuilder.append(attr.st.REPR.name);
                sigBuilder.append('(');
                if (!attr.st.REPR.box_description(tc, attr.st, sigBuilder)) forceNew = true;
                sigBuilder.append(')');
            }

            sigBuilder.append(';');
        }

        Class<?> use;
        if (forceNew) {
            use = generateJVMClass(tc, attrInfoList);
        } else {
            String sig = sigBuilder.toString();
            use = classCache.get(sig);
            if (use == null) {
                use = generateJVMClass(tc, attrInfoList);
                classCache.put(sig, use);
                //System.out.println("CREATE "+sig);
            } else {
                //System.out.println("REUSE "+sig);
            }
        }

        ((P6OpaqueREPRData)st.REPRData).jvmClass = use;
        try {
            ((P6OpaqueREPRData)st.REPRData).instance = (P6OpaqueBaseInstance)((P6OpaqueREPRData)st.REPRData).jvmClass.newInstance();
        }
        catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        ((P6OpaqueREPRData)st.REPRData).instance.st = st;
    }

    private Class<?> generateJVMClass(ThreadContext tc, List<AttrInfo> attrInfoList) {
        /* Create a unique name. */
        String className = "__P6opaque__" + typeId++;

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, className, null,
                "org/raku/nqp/sixmodel/reprs/P6OpaqueBaseInstance", null);

        Type tcType = Type.getType("Lorg/raku/nqp/runtime/ThreadContext;");
        Type smoType = Type.getType("Lorg/raku/nqp/sixmodel/SixModelObject;");
        Type srType = Type.getType("Lorg/raku/nqp/sixmodel/SerializationReader;");
        Type stType = Type.getType("Lorg/raku/nqp/sixmodel/STable;");

        /* bind_attribute_boxed */
        MethodVisitor bindBoxedVisitor;
        Label bindBoxedSwitch = null;
        Label[] bindBoxedLabels = null;
        Label bindBoxedDefault = new Label();
        {
            String descriptor = Type.getMethodDescriptor(Type.VOID_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE, smoType });
            bindBoxedVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "bind_attribute_boxed", descriptor, null, null);
            bindBoxedVisitor.visitCode();
            addDelegation(bindBoxedVisitor, "bind_attribute_boxed", Type.VOID_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE, smoType }, true);

            bindBoxedVisitor.visitVarInsn(Opcodes.LLOAD, 4);
            bindBoxedVisitor.visitInsn(Opcodes.L2I);

            if (attrInfoList.size() > 0) {
                bindBoxedLabels = new Label[attrInfoList.size()];
                for (int i = 0; i < attrInfoList.size(); i++) {
                    bindBoxedLabels[i] = new Label();
                }
                bindBoxedSwitch = new Label();
                bindBoxedVisitor.visitLabel(bindBoxedSwitch);
                bindBoxedVisitor.visitTableSwitchInsn(0, attrInfoList.size() - 1, bindBoxedDefault, bindBoxedLabels);
            }
        }

        /* bind_attribute_native */
        MethodVisitor bindNativeVisitor;
        Label bindNativeSwitch = null;
        Label[] bindNativeLabels = null;
        Label bindNativeDefault = new Label();
        {
            String descriptor = Type.getMethodDescriptor(Type.VOID_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE });
            bindNativeVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "bind_attribute_native", descriptor, null, null);
            bindNativeVisitor.visitCode();
            addDelegation(bindNativeVisitor, "bind_attribute_native", Type.VOID_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE }, false);

            bindNativeVisitor.visitVarInsn(Opcodes.LLOAD, 4);
            bindNativeVisitor.visitInsn(Opcodes.L2I);

            if (attrInfoList.size() > 0) {
                bindNativeLabels = new Label[attrInfoList.size()];
                for (int i = 0; i < attrInfoList.size(); i++) {
                    bindNativeLabels[i] = new Label();
                }
                bindNativeSwitch = new Label();
                bindNativeVisitor.visitLabel(bindNativeSwitch);
                bindNativeVisitor.visitTableSwitchInsn(0, attrInfoList.size() - 1, bindNativeDefault, bindNativeLabels);
            }
        }

        /* get_attribute_boxed */
        MethodVisitor getBoxedVisitor;
        Label getBoxedSwitch = null;
        Label[] getBoxedLabels = null;
        Label getBoxedDefault = new Label();
        {
            String descriptor = Type.getMethodDescriptor(smoType,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE });
            getBoxedVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "get_attribute_boxed", descriptor, null, null);
            getBoxedVisitor.visitCode();
            addDelegation(getBoxedVisitor, "get_attribute_boxed", smoType,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE }, false);

            getBoxedVisitor.visitVarInsn(Opcodes.LLOAD, 4);
            getBoxedVisitor.visitInsn(Opcodes.L2I);

            if (attrInfoList.size() > 0) {
                getBoxedLabels = new Label[attrInfoList.size()];
                for (int i = 0; i < attrInfoList.size(); i++) {
                    getBoxedLabels[i] = new Label();
                }
                getBoxedSwitch = new Label();
                getBoxedVisitor.visitLabel(getBoxedSwitch);
                getBoxedVisitor.visitTableSwitchInsn(0, attrInfoList.size() - 1, getBoxedDefault, getBoxedLabels);
            }
        }

        /* get_attribute_native */
        MethodVisitor getNativeVisitor;
        Label getNativeSwitch = null;
        Label[] getNativeLabels = null;
        Label getNativeDefault = new Label();
        {
            String descriptor = Type.getMethodDescriptor(Type.VOID_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE });
            getNativeVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "get_attribute_native", descriptor, null, null);
            getNativeVisitor.visitCode();
            addDelegation(getNativeVisitor, "get_attribute_native", Type.VOID_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE }, false);

            getNativeVisitor.visitVarInsn(Opcodes.LLOAD, 4);
            getNativeVisitor.visitInsn(Opcodes.L2I);

            if (attrInfoList.size() > 0) {
                getNativeLabels = new Label[attrInfoList.size()];
                for (int i = 0; i < attrInfoList.size(); i++) {
                    getNativeLabels[i] = new Label();
                }
                getNativeSwitch = new Label();
                getNativeVisitor.visitLabel(getNativeSwitch);
                getNativeVisitor.visitTableSwitchInsn(0, attrInfoList.size() - 1, getNativeDefault, getNativeLabels);
            }
        }

        /* is_attribute_initialized */
        MethodVisitor isInitVisitor;
        Label isInitSwitch = null;
        Label[] isInitLabels = null;
        Label isInitDefault = new Label();
        Label isInitNull = new Label();
        {
            String descriptor = Type.getMethodDescriptor(Type.LONG_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE });
            isInitVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "is_attribute_initialized", descriptor, null, null);
            isInitVisitor.visitCode();
            addDelegation(isInitVisitor, "is_attribute_initialized", Type.LONG_TYPE,
                    new Type[] { tcType, smoType, Type.getType(String.class), Type.LONG_TYPE }, false);

            isInitVisitor.visitVarInsn(Opcodes.LLOAD, 4);
            isInitVisitor.visitInsn(Opcodes.L2I);

            if (attrInfoList.size() > 0) {
                isInitLabels = new Label[attrInfoList.size()];
                for (int i = 0; i < attrInfoList.size(); i++) {
                    isInitLabels[i] = new Label();
                }
                isInitSwitch = new Label();
                isInitVisitor.visitLabel(isInitSwitch);
                isInitVisitor.visitTableSwitchInsn(0, attrInfoList.size() - 1, isInitDefault, isInitLabels);
            }
        }

        /* deserializeFields */
        MethodVisitor deserVisitor;
        {
            String descriptor = Type.getMethodDescriptor(Type.VOID_TYPE,
                    new Type[] { tcType, stType, srType });
            deserVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "deserializeFields", descriptor, null, null);
            deserVisitor.visitCode();
        }

        /* Now add all of the required fields and fill out the methods. */
        for (int i = 0; i < attrInfoList.size(); i++) {
            AttrInfo attr = attrInfoList.get(i);

            /* Is it a reference type or not? */
            StorageSpec ss = attr.st.REPR.get_storage_spec(tc, attr.st);
            if (ss.inlineable == StorageSpec.REFERENCE) {
                /* Add field. */
                String field = "field_" + i;
                String desc = "Lorg/raku/nqp/sixmodel/SixModelObject;";
                cw.visitField(Opcodes.ACC_PUBLIC, field, desc, null, null);

                /* Add bind code. */
                bindBoxedVisitor.visitLabel(bindBoxedLabels[i]);
                bindBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                bindBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 6);
                bindBoxedVisitor.visitFieldInsn(Opcodes.PUTFIELD, className, field, desc);
                bindBoxedVisitor.visitInsn(Opcodes.RETURN);

                /* Add get code. */
                getBoxedVisitor.visitLabel(getBoxedLabels[i]);
                getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                getBoxedVisitor.visitFieldInsn(Opcodes.GETFIELD, className, field, desc);

                if (attr.hasAutoVivContainer) {
                    Label end = new Label();
                    getBoxedVisitor.visitInsn(Opcodes.DUP);
                    getBoxedVisitor.visitJumpInsn(Opcodes.IFNONNULL, end);
                    getBoxedVisitor.visitInsn(Opcodes.POP);
                    getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                    getBoxedVisitor.visitIntInsn(Opcodes.BIPUSH, i);
                    getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 1);
                    String methodDesc = "(ILorg/raku/nqp/runtime/ThreadContext;)Lorg/raku/nqp/sixmodel/SixModelObject;";
                    getBoxedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "autoViv", methodDesc);
                    getBoxedVisitor.visitInsn(Opcodes.DUP);
                    getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                    getBoxedVisitor.visitInsn(Opcodes.SWAP);
                    getBoxedVisitor.visitFieldInsn(Opcodes.PUTFIELD, className, field, desc);
                    getBoxedVisitor.visitLabel(end);
                    getBoxedVisitor.visitInsn(Opcodes.ARETURN);
                }
                else {
                    getBoxedVisitor.visitInsn(Opcodes.ARETURN);
                }

                /* Add is init code. */
                isInitVisitor.visitLabel(isInitLabels[i]);
                isInitVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                isInitVisitor.visitFieldInsn(Opcodes.GETFIELD, className, field, desc);
                isInitVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "org/raku/nqp/runtime/Ops", "isnull", "(Lorg/raku/nqp/sixmodel/SixModelObject;)J");
                isInitVisitor.visitInsn(Opcodes.LCONST_1);
                isInitVisitor.visitInsn(Opcodes.LCMP);
                isInitVisitor.visitJumpInsn(Opcodes.IFEQ, isInitNull);
                isInitVisitor.visitInsn(Opcodes.ICONST_1);
                isInitVisitor.visitInsn(Opcodes.I2L);
                isInitVisitor.visitInsn(Opcodes.LRETURN);

                /* Native variants should just throw. */
                bindNativeVisitor.visitLabel(bindNativeLabels[i]);
                bindNativeVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                bindNativeVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "badNative", "()V");

                getNativeVisitor.visitLabel(getNativeLabels[i]);
                getNativeVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                getNativeVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "badNative", "()V");

                /* We deserialize these ourselves */
                deserVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                deserVisitor.visitVarInsn(Opcodes.ALOAD, 3);
                deserVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/raku/nqp/sixmodel/SerializationReader", "readRef", "()Lorg/raku/nqp/sixmodel/SixModelObject;");
                deserVisitor.visitFieldInsn(Opcodes.PUTFIELD, className, field, desc);
            }
            else {
                /* Generate field prefix and have target REPR install the field. */
                String prefix = "field_" + i;
                attr.st.REPR.inlineStorage(tc, attr.st, cw, prefix);

                /* Install bind/get instructions. */
                bindNativeVisitor.visitLabel(bindNativeLabels[i]);
                attr.st.REPR.inlineBind(tc, attr.st, bindNativeVisitor, className, prefix);
                getNativeVisitor.visitLabel(getNativeLabels[i]);
                attr.st.REPR.inlineGet(tc, attr.st, getNativeVisitor, className, prefix);

                /* Deserialization */
                attr.st.REPR.inlineDeserialize(tc, attr.st, deserVisitor, className, prefix);

                /* Add is init code. */
                isInitVisitor.visitLabel(isInitLabels[i]);
                isInitVisitor.visitInsn(Opcodes.ICONST_1);
                isInitVisitor.visitInsn(Opcodes.I2L);
                isInitVisitor.visitInsn(Opcodes.LRETURN);

                /* Reference variants should just throw. */
                bindBoxedVisitor.visitLabel(bindBoxedLabels[i]);
                bindBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                bindBoxedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "badReference", "()V");

                getBoxedVisitor.visitLabel(getBoxedLabels[i]);
                getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                getBoxedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "badReference", "()V");
            }

            /* If this is a box/unbox target, make sure it gets the appropriate
             * methods.
             */
            if (attr.boxTarget) {
                if (ss.inlineable == StorageSpec.REFERENCE)
                    throw ExceptionHandling.dieInternal(tc, "A box_target must not have a reference type attribute");
                attr.st.REPR.generateBoxingMethods(tc, attr.st, cw, className, "field_" + i);
            }

            /* If it's a positional or associative delegate, give it the methods
             * for that.
             */
            if (attr.posDelegate)
                generateDelegateMethod(tc, cw, className, "field_" + i, "posDelegate");
            if (attr.assDelegate)
                generateDelegateMethod(tc, cw, className, "field_" + i, "assDelegate");
        }

        /* Finish bind_boxed_attribute. */
        bindBoxedVisitor.visitLabel(bindBoxedDefault);
        bindBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        bindBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 2);
        bindBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 3);
        bindBoxedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "resolveAttribute",
                "(Lorg/raku/nqp/sixmodel/SixModelObject;Ljava/lang/String;)I");
        if (attrInfoList.size() > 0)
            bindBoxedVisitor.visitJumpInsn(Opcodes.GOTO, bindBoxedSwitch);
        else
            bindBoxedVisitor.visitInsn(Opcodes.RETURN);
        bindBoxedVisitor.visitMaxs(0, 0);
        bindBoxedVisitor.visitEnd();

        /* Finish bind_native_attribute. */
        bindNativeVisitor.visitLabel(bindNativeDefault);
        bindNativeVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        bindNativeVisitor.visitVarInsn(Opcodes.ALOAD, 2);
        bindNativeVisitor.visitVarInsn(Opcodes.ALOAD, 3);
        bindNativeVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "resolveAttribute",
                "(Lorg/raku/nqp/sixmodel/SixModelObject;Ljava/lang/String;)I");
        if (attrInfoList.size() > 0)
            bindNativeVisitor.visitJumpInsn(Opcodes.GOTO, bindNativeSwitch);
        else
            bindNativeVisitor.visitInsn(Opcodes.RETURN);
        bindNativeVisitor.visitMaxs(0, 0);
        bindNativeVisitor.visitEnd();

        /* Finish get_boxed_attribute. */
        getBoxedVisitor.visitLabel(getBoxedDefault);
        getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 2);
        getBoxedVisitor.visitVarInsn(Opcodes.ALOAD, 3);
        getBoxedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "resolveAttribute",
                "(Lorg/raku/nqp/sixmodel/SixModelObject;Ljava/lang/String;)I");
        if (attrInfoList.size() > 0)
            getBoxedVisitor.visitJumpInsn(Opcodes.GOTO, getBoxedSwitch);
        else
            getBoxedVisitor.visitInsn(Opcodes.ACONST_NULL);
        getBoxedVisitor.visitInsn(Opcodes.ARETURN);
        getBoxedVisitor.visitMaxs(0, 0);
        getBoxedVisitor.visitEnd();

        /* Finish get_native_attribute. */
        getNativeVisitor.visitLabel(getNativeDefault);
        getNativeVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        getNativeVisitor.visitVarInsn(Opcodes.ALOAD, 2);
        getNativeVisitor.visitVarInsn(Opcodes.ALOAD, 3);
        getNativeVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "resolveAttribute",
                "(Lorg/raku/nqp/sixmodel/SixModelObject;Ljava/lang/String;)I");
        if (attrInfoList.size() > 0)
            getNativeVisitor.visitJumpInsn(Opcodes.GOTO, getNativeSwitch);
        else
            getNativeVisitor.visitInsn(Opcodes.RETURN);
        getNativeVisitor.visitMaxs(6, 6);
        getNativeVisitor.visitEnd();

        /* Finish is_attribute_initialized. */
        isInitVisitor.visitLabel(isInitDefault);
        isInitVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        isInitVisitor.visitVarInsn(Opcodes.ALOAD, 2);
        isInitVisitor.visitVarInsn(Opcodes.ALOAD, 3);
        isInitVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, "resolveAttribute",
                "(Lorg/raku/nqp/sixmodel/SixModelObject;Ljava/lang/String;)I");
        if (attrInfoList.size() > 0)
            isInitVisitor.visitJumpInsn(Opcodes.GOTO, isInitSwitch);
        isInitVisitor.visitLabel(isInitNull);
        isInitVisitor.visitInsn(Opcodes.ICONST_0);
        isInitVisitor.visitInsn(Opcodes.I2L);
        isInitVisitor.visitInsn(Opcodes.LRETURN);
        isInitVisitor.visitMaxs(0, 0);
        isInitVisitor.visitEnd();

        /* Finish deserializeFields. */
        deserVisitor.visitInsn(Opcodes.RETURN);
        deserVisitor.visitMaxs(0, 0);
        deserVisitor.visitEnd();

        /* Finally, add empty constructor and generate the JVM storage class. */
        MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructor.visitCode();
        constructor.visitVarInsn(Opcodes.ALOAD, 0);
        constructor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                "org/raku/nqp/sixmodel/reprs/P6OpaqueBaseInstance", "<init>", "()V");
        constructor.visitInsn(Opcodes.RETURN);
        constructor.visitMaxs(1, 1);
        constructor.visitEnd();

        cw.visitEnd();

        byte[] classCompiled = cw.toByteArray();
        // Uncomment the following line to help debug the code-gen.
        if (System.getenv("NQP_DEBUG_DUMP_CLASSFILES") != null) {
            try {
                FileOutputStream fos = new FileOutputStream(new File(className + ".class"));
                fos.write(classCompiled);
                fos.close();
            } catch (IOException e) {
            }
        }
        return tc.gc.byteClassLoader.defineClass(className, classCompiled);
    }

    private void generateDelegateMethod(ThreadContext tc, ClassWriter cw, String className, String field, String methodName) {
        String desc = "Lorg/raku/nqp/sixmodel/SixModelObject;";

        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, methodName, "()"+desc, null, null);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitFieldInsn(Opcodes.GETFIELD, className, field, desc);
        mv.visitInsn(Opcodes.ARETURN);
        mv.visitMaxs(0, 0);
    }

    @Override
    public SixModelObject allocate(ThreadContext tc, STable st) {
        return ((P6OpaqueREPRData)st.REPRData).instance.instClone();
    }

    @Override
    public void change_type(ThreadContext tc, SixModelObject obj, SixModelObject newType) {
        // Ensure target type is also P6opaque-based.
        if (!(newType.st.REPR instanceof P6Opaque))
            throw ExceptionHandling.dieInternal(tc, "P6opaque can only rebless to another P6opaque-based type");

        // Ensure that the MROs overlap properly.
        P6OpaqueREPRData ourREPRData = (P6OpaqueREPRData)obj.st.REPRData;
        P6OpaqueREPRData targetREPRData = (P6OpaqueREPRData)newType.st.REPRData;
        if (ourREPRData.classHandles.length > targetREPRData.classHandles.length)
            throw ExceptionHandling.dieInternal(tc, "Incompatible MROs in P6opaque rebless");
        for (int i = 0; i < ourREPRData.classHandles.length; i++) {
            if (ourREPRData.classHandles[i] != targetREPRData.classHandles[i])
                throw ExceptionHandling.dieInternal(tc, "Incompatible MROs in P6opaque rebless");
        }

        // If there's a different number of attributes, need to set up delegate.
        // Note the condition below works because we don't make an entry in the
        // class handles list for a type with no attributes.
        P6OpaqueBaseInstance instance = (P6OpaqueBaseInstance)obj;
        if (ourREPRData.classHandles.length != targetREPRData.classHandles.length) {
            // Create delegate.
            SixModelObject delegate = newType.st.REPR.allocate(tc, newType.st);

            // Find original object.
            SixModelObject orig;
            if (Ops.isnull(instance.delegate) == 0)
                orig = instance.delegate;
            else
                orig = obj;

            // Copy over current attribute values.
            Field[] fromFields = orig.getClass().getFields();
            Field[] toFields = delegate.getClass().getFields();
            try {
                for (int i = 0; i < fromFields.length - 3; i++)
                    toFields[i].set(delegate, fromFields[i].get(orig));
            }
            catch (IllegalAccessException e) { throw new RuntimeException(e); }

            // Install.
            instance.delegate = delegate;
        }
        // If we don't have to create a new delegate, but there's a delegate
        // in place already, the delegate needs a new STable as well.
        // Otherwise, methods introduced in the new type won't be available.
        else if(Ops.isnull(instance.delegate) == 0) {
            instance.delegate.st = newType.st;
        }

        // Switch STable over to the new type.
        obj.st = newType.st;
    }

    @Override
    public StorageSpec get_storage_spec(ThreadContext tc, STable st) {
        P6OpaqueREPRData rd = (P6OpaqueREPRData)st.REPRData;
        StorageSpec ss = new StorageSpec();
        if (rd.unboxIntSlot >= 0)
            ss.can_box += StorageSpec.CAN_BOX_INT;
        if (rd.unboxNumSlot >= 0)
            ss.can_box += StorageSpec.CAN_BOX_NUM;
        if (rd.unboxStrSlot >= 0)
            ss.can_box += StorageSpec.CAN_BOX_STR;
        return ss;
    }

    @Override
    public long hint_for(ThreadContext tc, STable st, SixModelObject classHandle, String name) {
        P6OpaqueREPRData rd = (P6OpaqueREPRData)st.REPRData;
        for (int i = 0; i < rd.classHandles.length; i++) {
            if (rd.classHandles[i] == classHandle) {
                int idx = rd.nameToHintMap.get(i).getOrDefault(name, -1);
                if (idx != -1)
                    return idx;
                else
                    break;
            }
        }
        return STable.NO_HINT;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void deserialize_repr_data(ThreadContext tc, STable st, SerializationReader reader) {
        // Instantiate REPR data.
        P6OpaqueREPRData REPRData = new P6OpaqueREPRData();
        st.REPRData = REPRData;

        // Get attribute count.
        int numAttributes = (int)reader.readLong();

        // Get list of any flattened in STables.
        STable[] flattenedSTables = new STable[numAttributes];
        for (int i = 0; i < numAttributes; i++)
            if (reader.readLong() != 0)
                flattenedSTables[i] = reader.readSTableRef();
        REPRData.flattenedSTables = flattenedSTables;

        // Read "is multiple inheritance" flag; can go straight into data.
        REPRData.mi = reader.readLong() != 0;

        // Read any auto-viv values, if we have them.
        REPRData.autoVivContainers = new SixModelObject[numAttributes];
        if (reader.readLong() != 0) {
            for (int i = 0; i < numAttributes; i++)
                REPRData.autoVivContainers[i] = reader.readRef();
        }

        // Read unbox slot locations.
        REPRData.unboxIntSlot = (int)reader.readLong();
        REPRData.unboxNumSlot = (int)reader.readLong();
        REPRData.unboxStrSlot = (int)reader.readLong();

        // Read unbox object slot, if there is one.
        if (reader.readLong() != 0) {
            REPRData.unboxObjSlot = (int)reader.readLong();
        }

        // Read in the name to index mapping.
        int numClasses = (int)reader.readLong();
        ArrayList<SixModelObject> classHandles = new ArrayList<SixModelObject>();
        ArrayList<Object2IntOpenHashMap<String>> nameToHintMaps = new ArrayList<Object2IntOpenHashMap<String>>();
        for (int i = 0; i < numClasses; i++) {
            SixModelObject classHandle = reader.readRef();
            SixModelObject nameToHintObject = reader.readRef();
            if (Ops.isnull(nameToHintObject) == 1) {
                /* Nothing to do. */
            }
            else if (nameToHintObject instanceof VMHashInstance) {
                Object2IntOpenHashMap<String> nameToHintMap = new Object2IntOpenHashMap<String>();
                HashMap<String, SixModelObject> origMap = ((VMHashInstance)nameToHintObject).storage;
                if (origMap.size() > 0) {
                    for (String key : origMap.keySet())
                        nameToHintMap.put(key, (int)origMap.get(key).get_int(tc));
                    classHandles.add(classHandle);
                    nameToHintMaps.add(nameToHintMap);
                }
            }
            else {
                throw ExceptionHandling.dieInternal(tc, "Unexpected hint map representation in deserialize");
            }
        }
        REPRData.classHandles = classHandles.toArray(new SixModelObject[0]);
        REPRData.nameToHintMap = nameToHintMaps;

        // Read delegate slots.
        REPRData.posDelSlot = (int)reader.readLong();
        REPRData.assDelSlot = (int)reader.readLong();

        // Finally, reassemble the Java backing type.
        ArrayList<AttrInfo> attrInfoList = new ArrayList<AttrInfo>();
        for (int i = 0; i < numAttributes; i++) {
            AttrInfo info = new AttrInfo();
            if (flattenedSTables[i] != null)
                info.st = flattenedSTables[i];
            else
                info.st = tc.gc.KnowHOW.st; // Any reference type will do
            info.boxTarget = i == REPRData.unboxIntSlot || i == REPRData.unboxNumSlot ||
                    i == REPRData.unboxStrSlot || i == REPRData.unboxObjSlot;
            info.posDelegate = i == REPRData.posDelSlot;
            info.assDelegate = i == REPRData.assDelSlot;
            info.hasAutoVivContainer = REPRData.autoVivContainers[i] != null;
            attrInfoList.add(info);
        }
        if (numAttributes > 0) {
            installJVMType(tc, st, attrInfoList);
        }
        else {
            ((P6OpaqueREPRData)st.REPRData).jvmClass = P6OpaqueBaseInstance.class;
            ((P6OpaqueREPRData)st.REPRData).instance = new P6OpaqueBaseInstance();
            ((P6OpaqueREPRData)st.REPRData).instance.st = st;
        }
    }

    @Override
    public void serialize_repr_data(ThreadContext tc, STable st, SerializationWriter writer) {
        P6OpaqueREPRData REPRData = (P6OpaqueREPRData)st.REPRData;

        int numAttrs = REPRData.flattenedSTables.length;
        writer.writeInt(numAttrs);

        for (int i = 0; i < numAttrs; i++) {
            if (REPRData.flattenedSTables[i] == null) {
                writer.writeInt(0);
            }
            else {
                writer.writeInt(1);
                writer.writeSTableRef(REPRData.flattenedSTables[i]);
            }
        }

        writer.writeInt(REPRData.mi ? 1 : 0);

        if (REPRData.autoVivContainers != null) {
            writer.writeInt(1);
            for (int i = 0; i < numAttrs; i++)
               writer.writeRef(REPRData.autoVivContainers[i]);
        }
        else {
            writer.writeInt(0);
        }

        writer.writeInt(REPRData.unboxIntSlot);
        writer.writeInt(REPRData.unboxNumSlot);
        writer.writeInt(REPRData.unboxStrSlot);

        // Unbox slots
        if (REPRData.unboxObjSlot != -1) {
            writer.writeInt(1);
            writer.writeInt(REPRData.unboxObjSlot);
        }
        else {
            writer.writeInt(0);
        }

        int numClasses = REPRData.classHandles.length;
        writer.writeInt(numClasses);
        for (int i = 0; i < numClasses; i++) {
            writer.writeRef(REPRData.classHandles[i]);
            writer.writeIntHash(REPRData.nameToHintMap.get(i));
        }

        writer.writeInt(REPRData.posDelSlot);
        writer.writeInt(REPRData.assDelSlot);
    }

    @Override
    public SixModelObject deserialize_stub(ThreadContext tc, STable st) {
        P6OpaqueDelegateInstance stub = new P6OpaqueDelegateInstance();
        stub.st = st;
        return stub;
    }

    @Override
    public void deserialize_finish(ThreadContext tc, STable st,
                                   SerializationReader reader, SixModelObject stub) {
        // Create instance that we'll deserialize into.
        P6OpaqueBaseInstance obj = (P6OpaqueBaseInstance)((P6OpaqueREPRData)st.REPRData).instance.instClone();

        // Install it as the stub's delegate.
        ((P6OpaqueDelegateInstance)stub).delegate = obj;

        // Now deserialize all the fields.
        obj.deserializeFields(tc, st, reader);
    }

    @Override
    public void serialize(ThreadContext tc, SerializationWriter writer, SixModelObject origObj) {
        try {
            P6OpaqueBaseInstance obj = (P6OpaqueBaseInstance)origObj;
            if (Ops.isnull(obj.delegate) == 0)
                obj = (P6OpaqueBaseInstance)obj.delegate;
            STable[] flattenedSTables = ((P6OpaqueREPRData)obj.st.REPRData).flattenedSTables;
            if (flattenedSTables == null)
                throw ExceptionHandling.dieInternal(tc,
                    "Representation must be composed before it can be serialized");
            for (int i = 0; i < flattenedSTables.length; i++) {
                if (flattenedSTables[i] == null) {
                    writer.writeRef((SixModelObject)obj.getClass().getField("field_" + i).get(obj));
                }
                else {
                    flattenedSTables[i].REPR.serialize_inlined(tc, flattenedSTables[i],
                            writer, "field_" + i, obj);
                }
            }
        }
        catch (IllegalAccessException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }
}
