package org.raku.nqp.jast2bc;

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

import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

import java.util.Map;

import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import java.util.zip.CRC32;
import java.util.zip.ZipEntry;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
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.ThreadContext;

import org.raku.nqp.sixmodel.SixModelObject;
import org.raku.nqp.runtime.Ops;

import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4CompressorWithLength;

public class JASTCompiler {
    public static JavaClass buildClass(SixModelObject jast, SixModelObject jastNodes, boolean split, ThreadContext tc) {
        try {
            JASTCompiler compiler = new JASTCompiler(jastNodes, tc);
            JavaClass c = compiler.compileJast(jast, jastNodes, split, tc);
            return c;
        }
        catch (Exception e) {
            if (!split && "Method code too large!".equals(e.getMessage()))
                return buildClass(jast, jastNodes, true, tc);
            throw new RuntimeException(e);
        }
    }

    private static final LZ4CompressorWithLength lz4 =
        new LZ4CompressorWithLength(LZ4Factory.fastestInstance().highCompressor(8));
    // The lower the compression level, the faster the serialization, at the
    // expense of deserialization and disk space; the higher the compression
    // level, the lower the disk space, at the expense of both serialization
    // and deserialization. 8 is a sweet spot of sorts.

    public static void writeClass(SixModelObject jast, SixModelObject jastNodes, String filename, ThreadContext tc) {
        JavaClass c = buildClass(jast, jastNodes, false, tc);
        try {
            FileOutputStream fos = new FileOutputStream(filename);
            if (c.serialized == null) {
                // we're writing a plain java class
                fos.write(c.bytes);
                fos.close();
            } else {
                // writing a jar
                Manifest mf = new Manifest();
                mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
                if (c.hasMain)
                    mf.getMainAttributes().put(Attributes.Name.MAIN_CLASS, c.name);
                JarOutputStream jos = new JarOutputStream(fos, mf);

                JarEntry jec = new JarEntry(c.name + ".class");
                jec.setComment(c.name.replace(File.pathSeparatorChar, '.'));
                jos.putNextEntry(jec);
                jos.write(c.bytes);
                jos.closeEntry();

                byte[] digest = lz4.compress(c.serialized);
                CRC32 cipher = new CRC32();
                cipher.update(digest, 0, digest.length);

                JarEntry jes = new JarEntry(c.name + ".serialized.lz4");
                jes.setMethod(ZipEntry.STORED);
                jes.setSize(digest.length);
                jes.setCompressedSize(digest.length);
                jes.setCrc(cipher.getValue());
                jos.putNextEntry(jes);
                jos.write(digest);
                jos.closeEntry();

                jos.close();
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private JASTCompiler(SixModelObject jastNodes, ThreadContext tc) {
        if (!setup) setup(jastNodes, tc);

        jastLabel = jastNodes.at_key_boxed(tc, "JAST::Label");
        jastInstruction = jastNodes.at_key_boxed(tc, "JAST::Instruction");
        jastIndy = jastNodes.at_key_boxed(tc, "JAST::InvokeDynamic");
        jastInstructionList = jastNodes.at_key_boxed(tc, "JAST::InstructionList");
        jastPushI = jastNodes.at_key_boxed(tc, "JAST::PushIVal");
        jastPushN = jastNodes.at_key_boxed(tc, "JAST::PushNVal");
        jastPushS = jastNodes.at_key_boxed(tc, "JAST::PushSVal");
        jastPushC = jastNodes.at_key_boxed(tc, "JAST::PushCVal");
        jastPushIdx = jastNodes.at_key_boxed(tc, "JAST::PushIndex");
        jastTryCatch = jastNodes.at_key_boxed(tc, "JAST::TryCatch");
        jastAnnotation = jastNodes.at_key_boxed(tc, "JAST::Annotation");

    }

    private JavaClass compileJast(SixModelObject jast, SixModelObject jastNodes, boolean split, ThreadContext tc) throws Exception {

        SixModelObject jastClassObj = jastNodes.at_key_boxed(tc, "JAST::Class");
        SixModelObject jastField = jastNodes.at_key_boxed(tc, "JAST::Field");
        SixModelObject jastMethod = jastNodes.at_key_boxed(tc, "JAST::Method");

        JastClass jastClass = new JastClass(jast, jastClassObj, tc);
        JavaClass c = new JavaClass();

        c.name = jastClass.className;
        c.serialized = jastClass.serialized;

        String className = jastClass.className.replace('.', '/');
        String superName = jastClass.superName.replace('.', '/');

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, className, null,
                superName, null);
        cw.visitSource(jastClass.filename, null);

        SixModelObject iter = Ops.iter(jastClass.fields, tc);
        while (Ops.istrue(iter, tc) != 0) {
            JastField field = new JastField(iter.shift_boxed(tc), jastField, tc);

            cw.visitField(
                    field.isStatic
                        ? Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC
                        : Opcodes.ACC_PUBLIC,
                    field.name, field.type.getDescriptor(), null, null);
        }

        iter = Ops.iter(jastClass.methods, tc);
        while (Ops.istrue(iter, tc) != 0) {
            JastMethod method = new JastMethod(iter.shift_boxed(tc), jastMethod, tc);
            compileMethod(c, method, cw, className, split, tc);
        }

        // Add empty constructor.
        MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructor.visitCode();
        constructor.visitVarInsn(Opcodes.ALOAD, 0);
        constructor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                superName, "<init>", "()V");
        constructor.visitInsn(Opcodes.RETURN);
        constructor.visitMaxs(1, 1);
        constructor.visitEnd();

        cw.visitEnd();
        c.bytes = cw.toByteArray();

        return c;
    }

    private void compileMethod(JavaClass jcout, JastMethod method, ClassWriter c, String className, boolean split, ThreadContext tc) throws Exception {
        String desc = Type.getMethodDescriptor(method.returns, method.arguments.toArray(new Type[0]));
        int modifiers = method.isStatic? Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC: Opcodes.ACC_PUBLIC;
        MethodVisitor m = split ? new AutosplitMethodWriter(c, className, modifiers, method.name, desc, null, null) :
            c.visitMethod(modifiers, method.name, desc, null, null);

        if (method.name.equals("main")) jcout.hasMain = true;

        if ((method.crCuid != null && !method.crCuid.equals("")) || method.crOuter >= -1) {
            Type crAnnType = Type.getType("Lorg/raku/nqp/runtime/CodeRefAnnotation;");
            AnnotationVisitor av = m.visitAnnotation(crAnnType.getDescriptor(), true);
            av.visit("name", method.crName);
            if (method.crCuid != null && !method.crCuid.isEmpty()) av.visit("cuid", method.crCuid);
            if (method.crOuter >= 0) av.visit("outerQbid", method.crOuter);

            AnnotationVisitor avLex;
            if (method.crOlex.size() > 0) {
                avLex = av.visitArray("oLexicalNames");
                for (int i = 0; i < method.crOlex.size(); i++)
                    avLex.visit(null, method.crOlex.get(i));
                avLex.visitEnd();
            }
            if (method.crIlex.size() > 0) {
                avLex = av.visitArray("iLexicalNames");
                for (int i = 0; i < method.crIlex.size(); i++)
                    avLex.visit(null, method.crIlex.get(i));
                avLex.visitEnd();
            }
            if (method.crNlex.size() > 0) {
                avLex = av.visitArray("nLexicalNames");
                for (int i = 0; i < method.crNlex.size(); i++)
                    avLex.visit(null, method.crNlex.get(i));
                avLex.visitEnd();
            }
            if (method.crSlex.size() > 0) {
                avLex = av.visitArray("sLexicalNames");
                for (int i = 0; i < method.crSlex.size(); i++)
                    avLex.visit(null, method.crSlex.get(i));
                avLex.visitEnd();
            }

            if (method.crHandlers.length != 1 || method.crHandlers[0] != 0) av.visit("handlers", method.crHandlers);
            if (method.hasExitHandler) av.visit("hasExitHandler", method.hasExitHandler);
            if (method.argsExpectation > 0) av.visit("argsExpectation", method.argsExpectation);
            if (method.isThunk) av.visit("isThunk", method.isThunk);
            av.visitEnd();
        }

        if (!method.isStatic)
            method.locals.put("this", new VariableDef(0, "L"+className+";", method.beginAll, method.endAll));

        m.visitCode();
        m.visitLabel(method.beginAll);

        SixModelObject iter = Ops.iter(method.instructions, tc);
        while (Ops.istrue(iter, tc) != 0) {
            SixModelObject insn = iter.shift_boxed(tc);
            compileInstruction(insn, method, m, tc);
        }

        m.visitLabel(method.endAll);
        int i = 0;
        for (Map.Entry<String, VariableDef> e : method.locals.entrySet()) {
            VariableDef def = e.getValue();
            m.visitLocalVariable("__local_" + i++, def.type, null, def.start, def.end, def.index);
        }

        for (Map.Entry<String, LabelInfo> e : method.labels.entrySet()) {
            if (!e.getValue().defined)
                throw new Exception(e.getKey() + " used but not defined in " + method.name);
        }

        m.visitMaxs(0, 0);
        m.visitEnd();
    }

    private void compileInstruction(SixModelObject insn, JastMethod method, MethodVisitor m, ThreadContext tc) throws Exception {
        if (Ops.istype(insn, jastLabel, tc) != 0) {
            String labelName = Ops.getattr_s(insn, jastLabel, "$!name", 0, tc);
            if (!method.labels.containsKey(labelName))
                method.labels.put(labelName, new LabelInfo());
            LabelInfo inf = method.labels.get(labelName);
            if (inf.defined) throw new RuntimeException(labelName + " defined twice in " + method.name);
            inf.defined = true;
            m.visitLabel(inf.label);
        }
        else if (Ops.istype(insn, jastPushI, tc) != 0) {
            long value = Ops.getattr_i(insn, jastPushI, "$!value", 0, tc);
            if (value == 0) {
                m.visitInsn(Opcodes.LCONST_0);
            } else if (value == 1) {
                m.visitInsn(Opcodes.LCONST_1);
            } else {
                m.visitLdcInsn(value);
            }
        }
        else if (Ops.istype(insn, jastPushN, tc) != 0) {
            double value = Ops.getattr_n(insn, jastPushN, "$!value", 0, tc);
            m.visitLdcInsn(value);
        }
        else if (Ops.istype(insn, jastPushS, tc) != 0) {
            String value = Ops.getattr_s(insn, jastPushS, "$!value", 0, tc);
            m.visitLdcInsn(value);
        }
        else if (Ops.istype(insn, jastPushC, tc) != 0) {
            Type value = Type.getType(Ops.getattr_s(insn, jastPushC, "$!value", 0, tc));
            m.visitLdcInsn(value);
        }
        else if (Ops.istype(insn, jastPushIdx, tc) != 0) {
            int value = (int) Ops.getattr_i(insn, jastPushIdx, "$!value", 0, tc);
            if (value >= 0 && value <= 5) {
                m.visitInsn(Opcodes.ICONST_0 + value);
            }
            else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
                m.visitIntInsn(Opcodes.BIPUSH, value);
            }
            else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
                m.visitIntInsn(Opcodes.SIPUSH, value);
            }
            // bandaid for rakudo: reduce number of constant pool entries
            else if (value > Short.MAX_VALUE) {
                int value_remain = value - Short.MAX_VALUE;
                m.visitIntInsn(Opcodes.SIPUSH, Short.MAX_VALUE);
                while (value_remain > Short.MAX_VALUE) {
                    value_remain = value_remain - Short.MAX_VALUE;
                    m.visitIntInsn(Opcodes.SIPUSH, Short.MAX_VALUE);
                    m.visitInsn(Opcodes.IADD);
                }
                if (value_remain <= 5) {
                    m.visitInsn(Opcodes.ICONST_0 + value_remain);
                }
                else if (value_remain <= Byte.MAX_VALUE) {
                    m.visitIntInsn(Opcodes.BIPUSH, value_remain);
                }
                else {
                    m.visitIntInsn(Opcodes.SIPUSH, value_remain);
                }
                m.visitInsn(Opcodes.IADD);
            }
            else {
                m.visitLdcInsn(value);
            }
        }
        else if (Ops.istype(insn, jastTryCatch, tc) != 0) {
            Label start = new Label();
            Label afterCatch = new Label();
            Label end = new Label();
            Label handler = new Label();
            String typeName = Ops.getattr_s(insn, jastTryCatch, "$!type", 2, tc);
            if (typeName != null) {
                typeName = typeName.substring(1, typeName.length() - 1);
            }

            m.visitLabel(start);

            compileInstruction(Ops.getattr(insn, jastTryCatch, "$!try", 0, tc), method, m, tc);

            m.visitJumpInsn(Opcodes.GOTO, afterCatch);
            m.visitLabel(end);
            m.visitLabel(handler);
            m.visitTryCatchBlock(start, end, handler, typeName);

            compileInstruction(Ops.getattr(insn, jastTryCatch, "$!catch", 1, tc), method, m, tc);

            m.visitLabel(afterCatch);
        }
        else if (Ops.istype(insn, jastAnnotation, tc) != 0) {
            int line = (int) Ops.getattr_i(insn, jastAnnotation, "$!line", 0, tc);
            Label l = new Label();
            m.visitLabel(l);
            m.visitLineNumber(line, l);
        }
        else if (Ops.istype(insn, jastInstruction, tc) != 0) {
            emitInstruction(insn, method, m, tc);
        }
        else if (Ops.istype(insn, jastIndy, tc) != 0) {
            emitInvokeDynamic(insn, m, tc);
        }
        else if (Ops.istype(insn, jastInstructionList, tc) != 0) {
            SixModelObject iter = Ops.iter(Ops.getattr(insn, jastInstructionList, "@!instructions", 0, tc), tc);
            while (Ops.istrue(iter, tc) != 0) {
                compileInstruction(iter.shift_boxed(tc), method, m, tc);
            }
        }
        else {
            throw new Exception("Unknown JAST::Node in @!instructions: " + Ops.typeName(insn, tc));
        }
    }

    private void emitInstruction(SixModelObject insn, JastMethod method, MethodVisitor m, ThreadContext tc) throws Exception {
        int instruction = (int) Ops.getattr_i(insn, jastInstruction, "$!op", 0, tc);
        SixModelObject args = Ops.getattr(insn, jastInstruction, "@!args", 1, tc);

        // Go by instruction.
        switch (instruction) {
        case 0x00: // nop
        case 0x01: //aconst_null
        case 0x02: // iconst_m1
        case 0x03: // iconst_0
        case 0x04: // iconst_1
        case 0x05: // iconst_2
        case 0x06: // iconst_3
        case 0x07: // iconst_4
        case 0x08: // iconst_5
        case 0x09: // lconst_0
        case 0x0a: // lconst_1
        case 0x0b: // fconst_0
        case 0x0c: // fconst_1
        case 0x0d: // fconst_2
        case 0x0e: // dconst_0
        case 0x0f: // dconst_1
            m.visitInsn(instruction);
            break;
        case 0x12: // ldc
            // XXX: only supporting String constants currently
            String constant = Ops.atpos(args, 0, tc).get_str(tc);
            m.visitLdcInsn(constant);
            break;
        case 0x15: // iload
        case 0x16: // lload
        case 0x17: // fload
        case 0x18: // dload
        case 0x19: { // aload
            String name = Ops.atpos(args, 0, tc).get_str(tc);
            if (method.locals.containsKey(name))
                m.visitVarInsn(instruction, method.locals.get(name).index);
            else
                throw new Exception("Undeclared local variable: " + name);
            break;
        }
        case 0x1a: // iload_0
            m.visitVarInsn(Opcodes.ILOAD, 0);
            break;
        case 0x1b: // iload_1
            m.visitVarInsn(Opcodes.ILOAD, 1);
            break;
        case 0x1c: // iload_2
            m.visitVarInsn(Opcodes.ILOAD, 2);
            break;
        case 0x1d: // iload_3
            m.visitVarInsn(Opcodes.ILOAD, 3);
            break;
        case 0x1e: // lload_0
            m.visitVarInsn(Opcodes.LLOAD, 0);
            break;
        case 0x1f: // lload_1
            m.visitVarInsn(Opcodes.LLOAD, 1);
            break;
        case 0x20: // lload_2
            m.visitVarInsn(Opcodes.LLOAD, 2);
            break;
        case 0x21: // lload_3
            m.visitVarInsn(Opcodes.LLOAD, 3);
            break;
        case 0x22: // fload_0
            m.visitVarInsn(Opcodes.FLOAD, 0);
            break;
        case 0x23: // fload_1
            m.visitVarInsn(Opcodes.FLOAD, 1);
            break;
        case 0x24: // fload_2
            m.visitVarInsn(Opcodes.FLOAD, 2);
            break;
        case 0x25: // fload_3
            m.visitVarInsn(Opcodes.FLOAD, 3);
            break;
        case 0x26: // dload_0
            m.visitVarInsn(Opcodes.DLOAD, 0);
            break;
        case 0x27: // dload_1
            m.visitVarInsn(Opcodes.DLOAD, 1);
            break;
        case 0x28: // dload_2
            m.visitVarInsn(Opcodes.DLOAD, 2);
            break;
        case 0x29: // dload_3
            m.visitVarInsn(Opcodes.DLOAD, 3);
            break;
        case 0x2a: // aload_0
            m.visitVarInsn(Opcodes.ALOAD, 0);
            break;
        case 0x2b: // aload_1
            m.visitVarInsn(Opcodes.ALOAD, 1);
            break;
        case 0x2c: // aload_2
            m.visitVarInsn(Opcodes.ALOAD, 2);
            break;
        case 0x2d: // aload_3
            m.visitVarInsn(Opcodes.ALOAD, 3);
            break;
        case 0x2e: // iaload
        case 0x2f: // laload
        case 0x30: // faload
        case 0x31: // daload
        case 0x32: // aaload
        case 0x33: // baload
        case 0x34: // caload
        case 0x35: // saload
            m.visitInsn(instruction);
            break;
        case 0x36: // istore
        case 0x37: // lstore
        case 0x38: // fstore
        case 0x39: // dstore
        case 0x3a: { // astore
            String name = Ops.atpos(args, 0, tc).get_str(tc);
            if (method.locals.containsKey(name))
                m.visitVarInsn(instruction, method.locals.get(name).index);
            else
                throw new Exception("Undeclared local variable: " + name);
            break;
        }
        case 0x3b: // istore_0
            m.visitVarInsn(Opcodes.ISTORE, 0);
            break;
        case 0x3c: // istore_1
            m.visitVarInsn(Opcodes.ISTORE, 1);
            break;
        case 0x3d: // istore_2
            m.visitVarInsn(Opcodes.ISTORE, 2);
            break;
        case 0x3e: // istore_3
            m.visitVarInsn(Opcodes.ISTORE, 3);
            break;
        case 0x3f: // lstore_0
            m.visitVarInsn(Opcodes.LSTORE, 0);
            break;
        case 0x40: // lstore_1
            m.visitVarInsn(Opcodes.LSTORE, 1);
            break;
        case 0x41: // lstore_2
            m.visitVarInsn(Opcodes.LSTORE, 2);
            break;
        case 0x42: // lstore_3
            m.visitVarInsn(Opcodes.LSTORE, 3);
            break;
        case 0x43: // fstore_0
            m.visitVarInsn(Opcodes.FSTORE, 0);
            break;
        case 0x44: // fstore_1
            m.visitVarInsn(Opcodes.FSTORE, 1);
            break;
        case 0x45: // fstore_2
            m.visitVarInsn(Opcodes.FSTORE, 2);
            break;
        case 0x46: // fstore_3
            m.visitVarInsn(Opcodes.FSTORE, 3);
            break;
        case 0x47: // dstore_0
            m.visitVarInsn(Opcodes.DSTORE, 0);
            break;
        case 0x48: // dstore_1
            m.visitVarInsn(Opcodes.DSTORE, 1);
            break;
        case 0x49: // dstore_2
            m.visitVarInsn(Opcodes.DSTORE, 2);
            break;
        case 0x4a: // dstore_3
            m.visitVarInsn(Opcodes.DSTORE, 3);
            break;
        case 0x4b: // astore_0
            m.visitVarInsn(Opcodes.DSTORE, 0);
            break;
        case 0x4c: // astore_1
            m.visitVarInsn(Opcodes.DSTORE, 1);
            break;
        case 0x4d: // astore_2
            m.visitVarInsn(Opcodes.DSTORE, 2);
            break;
        case 0x4e: // astore_3
            m.visitVarInsn(Opcodes.DSTORE, 3);
            break;
        case 0x4f: // iastore
        case 0x50: // lastore
        case 0x51: // fastore
        case 0x52: // dastore
        case 0x53: // aastore
        case 0x54: // bastore
        case 0x55: // castore
        case 0x56: // sastore
        case 0x57: // pop
        case 0x58: // pop2
        case 0x59: // dup
        case 0x5a: // dup_x1
        case 0x5b: // dup_x2
        case 0x5c: // dup2
        case 0x5d: // dup2_x1
        case 0x5e: // dup2_x2
        case 0x5f: // swap
        case 0x60: // iadd
        case 0x61: // ladd
        case 0x62: // fadd
        case 0x63: // dadd
        case 0x64: // isub
        case 0x65: // lsub
        case 0x66: // fsub
        case 0x67: // dsub
        case 0x68: // imul
        case 0x69: // lmul
        case 0x6a: // fmul
        case 0x6b: // dmul
        case 0x6c: // idiv
        case 0x6d: // ldiv
        case 0x6e: // fdiv
        case 0x6f: // ddiv
        case 0x70: // irem
        case 0x71: // lrem
        case 0x72: // frem
        case 0x73: // drem
        case 0x74: // ineg
        case 0x75: // lneg
        case 0x76: // fneg
        case 0x77: // dneg
        case 0x78: // ishl
        case 0x79: // lshl
        case 0x7a: // ishr
        case 0x7b: // lshr
        case 0x7c: // iushr
        case 0x7d: // lushr
        case 0x7e: // iand
        case 0x7f: // land
        case 0x80: // ior
        case 0x81: // lor
        case 0x82: // ixor
        case 0x83: // lxor
        case 0x85: // i2l
        case 0x86: // i2f
        case 0x87: // i2d
        case 0x88: // l2i
        case 0x89: // l2f
        case 0x8a: // l2d
        case 0x8b: // f2i
        case 0x8c: // f2l
        case 0x8d: // f2d
        case 0x8e: // d2i
        case 0x8f: // d2l
        case 0x90: // d2f
        case 0x91: // i2b
        case 0x92: // i2c
        case 0x93: // i2s
        case 0x94: // lcmp
        case 0x95: // fcmpl
        case 0x96: // fcmpg
        case 0x97: // dcmpl
        case 0x98: // dcmpg
            m.visitInsn(instruction);
            break;
        case 0x99: // ifeq
        case 0x9a: // ifne
        case 0x9b: // iflt
        case 0x9c: // ifge
        case 0x9d: // ifgt
        case 0x9e: // ifle
        case 0x9f: // if_icmpeq
        case 0xa0: // if_icmpne
        case 0xa1: // if_icmplt
        case 0xa2: // if_icmpge
        case 0xa3: // if_icmpgt
        case 0xa4: // if_icmple
        case 0xa5: // if_acmpeq
        case 0xa6: // if_acmpne
        case 0xa7: // goto
            emitBranchInstruction(method, m, Ops.getattr_s(Ops.atpos(args, 0, tc), jastLabel, "$!name", 0, tc), instruction);
            break;
        case 0xaa: // tableswitch
            emitTableSwitchInstruction(method, m, args, tc);
            break;
        case 0xac: // ireturn
        case 0xad: // lreturn
        case 0xae: // freturn
        case 0xaf: // dreturn
        case 0xb0: // areturn
        case 0xb1: // return
            m.visitInsn(instruction);
            break;
        case 0xb2: // getstatic
        case 0xb3: // putstatic
        case 0xb4: // getfield
        case 0xb5: // putfield
            emitFieldAccess(m, args, instruction, tc);
            break;
        case 0xb6: // invokevirtual
        case 0xb7: // invokespecial
        case 0xb8: // invokestatic
            emitCall(m, args, instruction, tc);
            break;
        case 0xba:
            throw new Exception("Encountered invokedynamic in emitInstruction. This should never happen.");
        case 0xbb: // new
        case 0xc0: // checkcast
        case 0xc1: // instanceof
            Type t = processType(Ops.atpos(args, 0, tc).get_str(tc));
            m.visitTypeInsn(instruction, t.getInternalName());
            break;
        case 0xbc: { // newarray
            String name = Ops.atpos(args, 0, tc).get_str(tc);
            int type;
            if (name.equals("Integer"))
                type = Opcodes.T_INT;
            else if (name.equals("Long"))
                type = Opcodes.T_LONG;
            else if (name.equals("Double"))
                type = Opcodes.T_DOUBLE;
            else if (name.equals("Boolean"))
                type = Opcodes.T_BOOLEAN;
            else if (name.equals("J") || name.equals("Long"))
                type = Opcodes.T_LONG;
            else if (name.equals("Byte"))
                type = Opcodes.T_BYTE;
            else
                throw new RuntimeException("Unknown native array type");
            m.visitIntInsn(Opcodes.NEWARRAY, type);
            break;
        }
        case 0xbd: // anewarray
            m.visitTypeInsn(Opcodes.ANEWARRAY, processType(Ops.atpos(args, 0, tc).get_str(tc)).getInternalName());
            break;
        case 0xbe: // arraylength
            m.visitInsn(Opcodes.ARRAYLENGTH);
            break;
        case 0xbf: // athrow
            m.visitInsn(Opcodes.ATHROW);
            break;
        case 0xc6: // ifnull
        case 0xc7: // ifnonnull
        case 0xc8: // goto_w
            emitBranchInstruction(method, m, Ops.getattr_s(Ops.atpos(args, 0, tc), jastLabel, "$!name", 0, tc), instruction);
            break;
        default:
            throw new Exception("Unrecognized instruction #" + instruction);
        }
    }

    private void emitBranchInstruction(JastMethod method, MethodVisitor m, String label, int icode) {
        if (!method.labels.containsKey(label))
            method.labels.put(label, new LabelInfo());
        m.visitJumpInsn(icode, method.labels.get(label).label);
    }

    private void emitTableSwitchInstruction(JastMethod method, MethodVisitor m, SixModelObject args, ThreadContext tc) {
        int numKeys = (int) args.elems(tc);
        Label[] labels = new Label[numKeys - 1];
        Label defaultLabel = null;

        for (int i = 0; i < numKeys; i++) {
            String key = Ops.getattr_s(Ops.atpos(args, i, tc), jastLabel, "$!name", 0, tc);
            if (!method.labels.containsKey(key)) {
                method.labels.put(key, new LabelInfo());
            }
            if (i == 0) {
                defaultLabel = method.labels.get(key).label;
            } else {
                labels[i - 1] = method.labels.get(key).label;
            }
        }
        m.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);
    }

    private void emitFieldAccess(MethodVisitor m, SixModelObject args, int accessType, ThreadContext tc) {
        Type classType = processType(Ops.atpos(args, 0, tc).get_str(tc));
        String fieldName = Ops.atpos(args, 1, tc).get_str(tc);
        Type fieldType = processType(Ops.atpos(args, 2, tc).get_str(tc));
        m.visitFieldInsn(accessType, classType.getInternalName(), fieldName, fieldType.getDescriptor());
    }

    private void emitCall(MethodVisitor m, SixModelObject args, int callType, ThreadContext tc) {
        int argLen = (int) args.elems(tc);
        Type targetType = processType(Ops.atpos(args, 0, tc).get_str(tc));
        String methodName = Ops.atpos(args, 1, tc).get_str(tc);
        Type returnType = processType(Ops.atpos(args, 2, tc).get_str(tc));
        Type[] argumentTypes = new Type[argLen - 3];
        for (int i = 3; i < argLen; i++)
            argumentTypes[i - 3] = processType(Ops.atpos(args, i, tc).get_str(tc));
        m.visitMethodInsn(callType, targetType.getInternalName(), methodName,
                Type.getMethodDescriptor(returnType, argumentTypes));
    }

    private void emitInvokeDynamic(SixModelObject insn, MethodVisitor m, ThreadContext tc) {
        String name = Ops.getattr(insn, jastIndy, "$!name", 0, tc).get_str(tc);
        SixModelObject argTypesSmo = Ops.getattr(insn, jastIndy, "@!arg_types", 1, tc);
        Type retType = processType(Ops.getattr(insn, jastIndy, "$!ret_type", 2, tc).get_str(tc));
        String bsmType = Ops.getattr(insn, jastIndy, "$!bsm_type", 3, tc).get_str(tc);
        String bsmName = Ops.getattr(insn, jastIndy, "$!bsm_name", 4, tc).get_str(tc);
        SixModelObject extraArgsSmo = Ops.getattr(insn, jastIndy, "@!extra_args", 5, tc);

        int numArgs = (int) argTypesSmo.elems(tc);
        Type[] argTypes = new Type[numArgs];
        for (int i = 0; i < numArgs; i++) {
            argTypes[i] = processType(Ops.atpos(argTypesSmo, i, tc).get_str(tc));
        }

        int numExtraArgs = (int) extraArgsSmo.elems(tc);
        MethodType bsmMT = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class,
                java.lang.String.class, MethodType.class);

        Object[] extraArgs = new Object[numExtraArgs];
        for (int i = 0; i < numExtraArgs; i++) {
            SixModelObject extra = Ops.atpos(extraArgsSmo, i, tc);
            if (Ops.istype(extra, jastPushI, tc) != 0) {
                extraArgs[i] = Ops.getattr_i(extra, jastPushI, "$!value", 0, tc);
                bsmMT = bsmMT.appendParameterTypes(long.class);
            }
            else if (Ops.istype(extra, jastPushN, tc) != 0) {
                extraArgs[i] = Ops.getattr_n(extra, jastPushN, "$!value", 0, tc);
                bsmMT = bsmMT.appendParameterTypes(double.class);
            }
            else if (Ops.istype(extra, jastPushS, tc) != 0) {
                extraArgs[i] = Ops.getattr_s(extra, jastPushS, "$!value", 0, tc);
                bsmMT = bsmMT.appendParameterTypes(String.class);
            }
            else if (Ops.istype(extra, jastPushIdx, tc) != 0) {
                extraArgs[i] = (int) Ops.getattr_i(extra, jastPushIdx, "$!value", 0, tc);
                bsmMT = bsmMT.appendParameterTypes(int.class);
            }
            else {
                throw new RuntimeException("Unrecognized extra argument for invokedynamic");
            }
        }

        Handle bsmHandle = new Handle(Opcodes.H_INVOKESTATIC, bsmType, bsmName, bsmMT.toMethodDescriptorString());
        m.visitInvokeDynamicInsn(name, Type.getMethodDescriptor(retType, argTypes), bsmHandle, extraArgs);
    }

    private static boolean setup = false;
    private SixModelObject jastLabel, jastInstruction, jastInstructionList,
            jastIndy, jastPushI, jastPushIdx, jastPushN, jastPushS, jastPushC,
            jastTryCatch, jastAnnotation;
    private static void setup(SixModelObject jastNodes, ThreadContext tc) {
        setup = true;

        JastClass.setup(jastNodes.at_key_boxed(tc, "JAST::Class"), tc);
        JastField.setup(jastNodes.at_key_boxed(tc, "JAST::Field"), tc);
        JastMethod.setup(jastNodes.at_key_boxed(tc, "JAST::Method"), tc);
    }

    public static Type processType(String typeName) {
        // Long needs special treatment; getType doesn't cope with it.
        if (typeName.equals("Long"))
            return Type.LONG_TYPE;
        return Type.getType(typeName);
    }

    static public class LabelInfo {
        public Label label = new Label();
        public boolean defined;
    }

    static public class VariableDef {
        public VariableDef(int i, String t, Label s, Label e) {
            index = i;
            type = t;
            start = s;
            end = e;
        }

        public final int index;
        public final String type;
        public final Label start;
        public final Label end;
    }
}
