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

package com.android.dx.cf.code;

import com.android.dx.cf.iface.ParseException;
import com.android.dx.rop.cst.Constant;
import com.android.dx.rop.cst.ConstantPool;
import com.android.dx.rop.cst.CstDouble;
import com.android.dx.rop.cst.CstFloat;
import com.android.dx.rop.cst.CstInteger;
import com.android.dx.rop.cst.CstKnownNull;
import com.android.dx.rop.cst.CstLiteralBits;
import com.android.dx.rop.cst.CstLong;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.Type;
import com.android.dx.util.Bits;
import com.android.dx.util.ByteArray;
import com.android.dx.util.Hex;
import java.util.ArrayList;

/**
 * Bytecode array, which is part of a standard {@code Code} attribute.
 */
public final class BytecodeArray {
    /** convenient no-op implementation of {@link Visitor} */
    public static final Visitor EMPTY_VISITOR = new BaseVisitor();

    /** {@code non-null;} underlying bytes */
    private final ByteArray bytes;

    /**
     * {@code non-null;} constant pool to use when resolving constant
     * pool indices
     */
    private final ConstantPool pool;

    /**
     * Constructs an instance.
     *
     * @param bytes {@code non-null;} underlying bytes
     * @param pool {@code non-null;} constant pool to use when
     * resolving constant pool indices
     */
    public BytecodeArray(ByteArray bytes, ConstantPool pool) {
        if (bytes == null) {
            throw new NullPointerException("bytes == null");
        }

        if (pool == null) {
            throw new NullPointerException("pool == null");
        }

        this.bytes = bytes;
        this.pool = pool;
    }

    /**
     * Gets the underlying byte array.
     *
     * @return {@code non-null;} the byte array
     */
    public ByteArray getBytes() {
        return bytes;
    }

    /**
     * Gets the size of the bytecode array, per se.
     *
     * @return {@code >= 0;} the length of the bytecode array
     */
    public int size() {
        return bytes.size();
    }

    /**
     * Gets the total length of this structure in bytes, when included in
     * a {@code Code} attribute. The returned value includes the
     * array size plus four bytes for {@code code_length}.
     *
     * @return {@code >= 4;} the total length, in bytes
     */
    public int byteLength() {
        return 4 + bytes.size();
    }

    /**
     * Parses each instruction in the array, in order.
     *
     * @param visitor {@code null-ok;} visitor to call back to for
     * each instruction
     */
    public void forEach(Visitor visitor) {
        int sz = bytes.size();
        int at = 0;

        while (at < sz) {
            /*
             * Don't record the previous offset here, so that we get to see the
             * raw code that initializes the array
             */
            at += parseInstruction(at, visitor);
        }
    }

    /**
     * Finds the offset to each instruction in the bytecode array. The
     * result is a bit set with the offset of each opcode-per-se flipped on.
     *
     * @see Bits
     * @return {@code non-null;} appropriately constructed bit set
     */
    public int[] getInstructionOffsets() {
        int sz = bytes.size();
        int[] result = Bits.makeBitSet(sz);
        int at = 0;

        while (at < sz) {
            Bits.set(result, at, true);
            int length = parseInstruction(at, null);
            at += length;
        }

        return result;
    }

    /**
     * Processes the given "work set" by repeatedly finding the lowest bit
     * in the set, clearing it, and parsing and visiting the instruction at
     * the indicated offset (that is, the bit index), repeating until the
     * work set is empty. It is expected that the visitor will regularly
     * set new bits in the work set during the process.
     *
     * @param workSet {@code non-null;} the work set to process
     * @param visitor {@code non-null;} visitor to call back to for
     * each instruction
     */
    public void processWorkSet(int[] workSet, Visitor visitor) {
        if (visitor == null) {
            throw new NullPointerException("visitor == null");
        }

        for (;;) {
            int offset = Bits.findFirst(workSet, 0);
            if (offset < 0) {
                break;
            }
            Bits.clear(workSet, offset);
            parseInstruction(offset, visitor);
            visitor.setPreviousOffset(offset);
        }
    }

    /**
     * Parses the instruction at the indicated offset. Indicate the
     * result by calling the visitor if supplied and by returning the
     * number of bytes consumed by the instruction.
     *
     * <p>In order to simplify further processing, the opcodes passed
     * to the visitor are canonicalized, altering the opcode to a more
     * universal one and making formerly implicit arguments
     * explicit. In particular:</p>
     *
     * <ul>
     * <li>The opcodes to push literal constants of primitive types all become
     *   {@code ldc}.
     *   E.g., {@code fconst_0}, {@code sipush}, and
     *   {@code lconst_0} qualify for this treatment.</li>
     * <li>{@code aconst_null} becomes {@code ldc} of a
     *   "known null."</li>
     * <li>Shorthand local variable accessors become the corresponding
     *   longhand. E.g. {@code aload_2} becomes {@code aload}.</li>
     * <li>{@code goto_w} and {@code jsr_w} become {@code goto}
     *   and {@code jsr} (respectively).</li>
     * <li>{@code ldc_w} becomes {@code ldc}.</li>
     * <li>{@code tableswitch} becomes {@code lookupswitch}.
     * <li>Arithmetic, array, and value-returning ops are collapsed
     *   to the {@code int} variant opcode, with the {@code type}
     *   argument set to indicate the actual type. E.g.,
     *   {@code fadd} becomes {@code iadd}, but
     *   {@code type} is passed as {@code Type.FLOAT} in that
     *   case. Similarly, {@code areturn} becomes
     *   {@code ireturn}. (However, {@code return} remains
     *   unchanged.</li>
     * <li>Local variable access ops are collapsed to the {@code int}
     *   variant opcode, with the {@code type} argument set to indicate
     *   the actual type. E.g., {@code aload} becomes {@code iload},
     *   but {@code type} is passed as {@code Type.OBJECT} in
     *   that case.</li>
     * <li>Numeric conversion ops ({@code i2l}, etc.) are left alone
     *   to avoid too much confustion, but their {@code type} is
     *   the pushed type. E.g., {@code i2b} gets type
     *   {@code Type.INT}, and {@code f2d} gets type
     *   {@code Type.DOUBLE}. Other unaltered opcodes also get
     *   their pushed type. E.g., {@code arraylength} gets type
     *   {@code Type.INT}.</li>
     * </ul>
     *
     * @param offset {@code >= 0, < bytes.size();} offset to the start of the
     * instruction
     * @param visitor {@code null-ok;} visitor to call back to
     * @return the length of the instruction, in bytes
     */
    public int parseInstruction(int offset, Visitor visitor) {
        if (visitor == null) {
            visitor = EMPTY_VISITOR;
        }

        try {
            int opcode = bytes.getUnsignedByte(offset);
            int info = ByteOps.opInfo(opcode);
            int fmt = info & ByteOps.FMT_MASK;

            switch (opcode) {
                case ByteOps.NOP: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.VOID);
                    return 1;
                }
                case ByteOps.ACONST_NULL: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstKnownNull.THE_ONE, 0);
                    return 1;
                }
                case ByteOps.ICONST_M1: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_M1, -1);
                    return 1;
                }
                case ByteOps.ICONST_0: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_0, 0);
                    return 1;
                }
                case ByteOps.ICONST_1: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_1, 1);
                    return 1;
                }
                case ByteOps.ICONST_2: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_2, 2);
                    return 1;
                }
                case ByteOps.ICONST_3: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_3, 3);
                    return 1;
                }
                case ByteOps.ICONST_4: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_4, 4);
                    return 1;
                }
                case ByteOps.ICONST_5:  {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstInteger.VALUE_5, 5);
                    return 1;
                }
                case ByteOps.LCONST_0: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstLong.VALUE_0, 0);
                    return 1;
                }
                case ByteOps.LCONST_1: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstLong.VALUE_1, 0);
                    return 1;
                }
                case ByteOps.FCONST_0: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstFloat.VALUE_0, 0);
                    return 1;
                }
                case ByteOps.FCONST_1: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstFloat.VALUE_1, 0);
                    return 1;
                }
                case ByteOps.FCONST_2:  {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstFloat.VALUE_2, 0);
                    return 1;
                }
                case ByteOps.DCONST_0: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstDouble.VALUE_0, 0);
                    return 1;
                }
                case ByteOps.DCONST_1: {
                    visitor.visitConstant(ByteOps.LDC, offset, 1,
                                          CstDouble.VALUE_1, 0);
                    return 1;
                }
                case ByteOps.BIPUSH: {
                    int value = bytes.getByte(offset + 1);
                    visitor.visitConstant(ByteOps.LDC, offset, 2,
                                          CstInteger.make(value), value);
                    return 2;
                }
                case ByteOps.SIPUSH: {
                    int value = bytes.getShort(offset + 1);
                    visitor.visitConstant(ByteOps.LDC, offset, 3,
                                          CstInteger.make(value), value);
                    return 3;
                }
                case ByteOps.LDC: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    Constant cst = pool.get(idx);
                    int value = (cst instanceof CstInteger) ?
                        ((CstInteger) cst).getValue() : 0;
                    visitor.visitConstant(ByteOps.LDC, offset, 2, cst, value);
                    return 2;
                }
                case ByteOps.LDC_W: {
                    int idx = bytes.getUnsignedShort(offset + 1);
                    Constant cst = pool.get(idx);
                    int value = (cst instanceof CstInteger) ?
                        ((CstInteger) cst).getValue() : 0;
                    visitor.visitConstant(ByteOps.LDC, offset, 3, cst, value);
                    return 3;
                }
                case ByteOps.LDC2_W: {
                    int idx = bytes.getUnsignedShort(offset + 1);
                    Constant cst = pool.get(idx);
                    visitor.visitConstant(ByteOps.LDC2_W, offset, 3, cst, 0);
                    return 3;
                }
                case ByteOps.ILOAD: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx,
                                       Type.INT, 0);
                    return 2;
                }
                case ByteOps.LLOAD: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx,
                                       Type.LONG, 0);
                    return 2;
                }
                case ByteOps.FLOAD: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx,
                                       Type.FLOAT, 0);
                    return 2;
                }
                case ByteOps.DLOAD: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx,
                                       Type.DOUBLE, 0);
                    return 2;
                }
                case ByteOps.ALOAD: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx,
                                       Type.OBJECT, 0);
                    return 2;
                }
                case ByteOps.ILOAD_0:
                case ByteOps.ILOAD_1:
                case ByteOps.ILOAD_2:
                case ByteOps.ILOAD_3: {
                    int idx = opcode - ByteOps.ILOAD_0;
                    visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx,
                                       Type.INT, 0);
                    return 1;
                }
                case ByteOps.LLOAD_0:
                case ByteOps.LLOAD_1:
                case ByteOps.LLOAD_2:
                case ByteOps.LLOAD_3: {
                    int idx = opcode - ByteOps.LLOAD_0;
                    visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx,
                                       Type.LONG, 0);
                    return 1;
                }
                case ByteOps.FLOAD_0:
                case ByteOps.FLOAD_1:
                case ByteOps.FLOAD_2:
                case ByteOps.FLOAD_3: {
                    int idx = opcode - ByteOps.FLOAD_0;
                    visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx,
                                       Type.FLOAT, 0);
                    return 1;
                }
                case ByteOps.DLOAD_0:
                case ByteOps.DLOAD_1:
                case ByteOps.DLOAD_2:
                case ByteOps.DLOAD_3: {
                    int idx = opcode - ByteOps.DLOAD_0;
                    visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx,
                                       Type.DOUBLE, 0);
                    return 1;
                }
                case ByteOps.ALOAD_0:
                case ByteOps.ALOAD_1:
                case ByteOps.ALOAD_2:
                case ByteOps.ALOAD_3: {
                    int idx = opcode - ByteOps.ALOAD_0;
                    visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx,
                                       Type.OBJECT, 0);
                    return 1;
                }
                case ByteOps.IALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.INT);
                    return 1;
                }
                case ByteOps.LALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.LONG);
                    return 1;
                }
                case ByteOps.FALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1,
                                        Type.FLOAT);
                    return 1;
                }
                case ByteOps.DALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1,
                                        Type.DOUBLE);
                    return 1;
                }
                case ByteOps.AALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1,
                                        Type.OBJECT);
                    return 1;
                }
                case ByteOps.BALOAD: {
                    /*
                     * Note: This is a load from either a byte[] or a
                     * boolean[].
                     */
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.BYTE);
                    return 1;
                }
                case ByteOps.CALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.CHAR);
                    return 1;
                }
                case ByteOps.SALOAD: {
                    visitor.visitNoArgs(ByteOps.IALOAD, offset, 1,
                                        Type.SHORT);
                    return 1;
                }
                case ByteOps.ISTORE: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx,
                                       Type.INT, 0);
                    return 2;
                }
                case ByteOps.LSTORE: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx,
                                       Type.LONG, 0);
                    return 2;
                }
                case ByteOps.FSTORE: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx,
                                       Type.FLOAT, 0);
                    return 2;
                }
                case ByteOps.DSTORE: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx,
                                       Type.DOUBLE, 0);
                    return 2;
                }
                case ByteOps.ASTORE: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx,
                                       Type.OBJECT, 0);
                    return 2;
                }
                case ByteOps.ISTORE_0:
                case ByteOps.ISTORE_1:
                case ByteOps.ISTORE_2:
                case ByteOps.ISTORE_3: {
                    int idx = opcode - ByteOps.ISTORE_0;
                    visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx,
                                       Type.INT, 0);
                    return 1;
                }
                case ByteOps.LSTORE_0:
                case ByteOps.LSTORE_1:
                case ByteOps.LSTORE_2:
                case ByteOps.LSTORE_3: {
                    int idx = opcode - ByteOps.LSTORE_0;
                    visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx,
                                       Type.LONG, 0);
                    return 1;
                }
                case ByteOps.FSTORE_0:
                case ByteOps.FSTORE_1:
                case ByteOps.FSTORE_2:
                case ByteOps.FSTORE_3: {
                    int idx = opcode - ByteOps.FSTORE_0;
                    visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx,
                                       Type.FLOAT, 0);
                    return 1;
                }
                case ByteOps.DSTORE_0:
                case ByteOps.DSTORE_1:
                case ByteOps.DSTORE_2:
                case ByteOps.DSTORE_3: {
                    int idx = opcode - ByteOps.DSTORE_0;
                    visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx,
                                       Type.DOUBLE, 0);
                    return 1;
                }
                case ByteOps.ASTORE_0:
                case ByteOps.ASTORE_1:
                case ByteOps.ASTORE_2:
                case ByteOps.ASTORE_3: {
                    int idx = opcode - ByteOps.ASTORE_0;
                    visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx,
                                       Type.OBJECT, 0);
                    return 1;
                }
                case ByteOps.IASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, Type.INT);
                    return 1;
                }
                case ByteOps.LASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.LONG);
                    return 1;
                }
                case ByteOps.FASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.FLOAT);
                    return 1;
                }
                case ByteOps.DASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.DOUBLE);
                    return 1;
                }
                case ByteOps.AASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.OBJECT);
                    return 1;
                }
                case ByteOps.BASTORE: {
                    /*
                     * Note: This is a load from either a byte[] or a
                     * boolean[].
                     */
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.BYTE);
                    return 1;
                }
                case ByteOps.CASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.CHAR);
                    return 1;
                }
                case ByteOps.SASTORE: {
                    visitor.visitNoArgs(ByteOps.IASTORE, offset, 1,
                                        Type.SHORT);
                    return 1;
                }
                case ByteOps.POP:
                case ByteOps.POP2:
                case ByteOps.DUP:
                case ByteOps.DUP_X1:
                case ByteOps.DUP_X2:
                case ByteOps.DUP2:
                case ByteOps.DUP2_X1:
                case ByteOps.DUP2_X2:
                case ByteOps.SWAP: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.VOID);
                    return 1;
                }
                case ByteOps.IADD:
                case ByteOps.ISUB:
                case ByteOps.IMUL:
                case ByteOps.IDIV:
                case ByteOps.IREM:
                case ByteOps.INEG:
                case ByteOps.ISHL:
                case ByteOps.ISHR:
                case ByteOps.IUSHR:
                case ByteOps.IAND:
                case ByteOps.IOR:
                case ByteOps.IXOR: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.INT);
                    return 1;
                }
                case ByteOps.LADD:
                case ByteOps.LSUB:
                case ByteOps.LMUL:
                case ByteOps.LDIV:
                case ByteOps.LREM:
                case ByteOps.LNEG:
                case ByteOps.LSHL:
                case ByteOps.LSHR:
                case ByteOps.LUSHR:
                case ByteOps.LAND:
                case ByteOps.LOR:
                case ByteOps.LXOR: {
                    /*
                     * It's "opcode - 1" because, conveniently enough, all
                     * these long ops are one past the int variants.
                     */
                    visitor.visitNoArgs(opcode - 1, offset, 1, Type.LONG);
                    return 1;
                }
                case ByteOps.FADD:
                case ByteOps.FSUB:
                case ByteOps.FMUL:
                case ByteOps.FDIV:
                case ByteOps.FREM:
                case ByteOps.FNEG: {
                    /*
                     * It's "opcode - 2" because, conveniently enough, all
                     * these float ops are two past the int variants.
                     */
                    visitor.visitNoArgs(opcode - 2, offset, 1, Type.FLOAT);
                    return 1;
                }
                case ByteOps.DADD:
                case ByteOps.DSUB:
                case ByteOps.DMUL:
                case ByteOps.DDIV:
                case ByteOps.DREM:
                case ByteOps.DNEG: {
                    /*
                     * It's "opcode - 3" because, conveniently enough, all
                     * these double ops are three past the int variants.
                     */
                    visitor.visitNoArgs(opcode - 3, offset, 1, Type.DOUBLE);
                    return 1;
                }
                case ByteOps.IINC: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    int value = bytes.getByte(offset + 2);
                    visitor.visitLocal(opcode, offset, 3, idx,
                                       Type.INT, value);
                    return 3;
                }
                case ByteOps.I2L:
                case ByteOps.F2L:
                case ByteOps.D2L: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.LONG);
                    return 1;
                }
                case ByteOps.I2F:
                case ByteOps.L2F:
                case ByteOps.D2F: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.FLOAT);
                    return 1;
                }
                case ByteOps.I2D:
                case ByteOps.L2D:
                case ByteOps.F2D: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.DOUBLE);
                    return 1;
                }
                case ByteOps.L2I:
                case ByteOps.F2I:
                case ByteOps.D2I:
                case ByteOps.I2B:
                case ByteOps.I2C:
                case ByteOps.I2S:
                case ByteOps.LCMP:
                case ByteOps.FCMPL:
                case ByteOps.FCMPG:
                case ByteOps.DCMPL:
                case ByteOps.DCMPG:
                case ByteOps.ARRAYLENGTH: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.INT);
                    return 1;
                }
                case ByteOps.IFEQ:
                case ByteOps.IFNE:
                case ByteOps.IFLT:
                case ByteOps.IFGE:
                case ByteOps.IFGT:
                case ByteOps.IFLE:
                case ByteOps.IF_ICMPEQ:
                case ByteOps.IF_ICMPNE:
                case ByteOps.IF_ICMPLT:
                case ByteOps.IF_ICMPGE:
                case ByteOps.IF_ICMPGT:
                case ByteOps.IF_ICMPLE:
                case ByteOps.IF_ACMPEQ:
                case ByteOps.IF_ACMPNE:
                case ByteOps.GOTO:
                case ByteOps.JSR:
                case ByteOps.IFNULL:
                case ByteOps.IFNONNULL: {
                    int target = offset + bytes.getShort(offset + 1);
                    visitor.visitBranch(opcode, offset, 3, target);
                    return 3;
                }
                case ByteOps.RET: {
                    int idx = bytes.getUnsignedByte(offset + 1);
                    visitor.visitLocal(opcode, offset, 2, idx,
                                       Type.RETURN_ADDRESS, 0);
                    return 2;
                }
                case ByteOps.TABLESWITCH: {
                    return parseTableswitch(offset, visitor);
                }
                case ByteOps.LOOKUPSWITCH: {
                    return parseLookupswitch(offset, visitor);
                }
                case ByteOps.IRETURN: {
                    visitor.visitNoArgs(ByteOps.IRETURN, offset, 1, Type.INT);
                    return 1;
                }
                case ByteOps.LRETURN: {
                    visitor.visitNoArgs(ByteOps.IRETURN, offset, 1,
                                        Type.LONG);
                    return 1;
                }
                case ByteOps.FRETURN: {
                    visitor.visitNoArgs(ByteOps.IRETURN, offset, 1,
                                        Type.FLOAT);
                    return 1;
                }
                case ByteOps.DRETURN: {
                    visitor.visitNoArgs(ByteOps.IRETURN, offset, 1,
                                        Type.DOUBLE);
                    return 1;
                }
                case ByteOps.ARETURN: {
                    visitor.visitNoArgs(ByteOps.IRETURN, offset, 1,
                                        Type.OBJECT);
                    return 1;
                }
                case ByteOps.RETURN:
                case ByteOps.ATHROW:
                case ByteOps.MONITORENTER:
                case ByteOps.MONITOREXIT: {
                    visitor.visitNoArgs(opcode, offset, 1, Type.VOID);
                    return 1;
                }
                case ByteOps.GETSTATIC:
                case ByteOps.PUTSTATIC:
                case ByteOps.GETFIELD:
                case ByteOps.PUTFIELD:
                case ByteOps.INVOKEVIRTUAL:
                case ByteOps.INVOKESPECIAL:
                case ByteOps.INVOKESTATIC:
                case ByteOps.NEW:
                case ByteOps.ANEWARRAY:
                case ByteOps.CHECKCAST:
                case ByteOps.INSTANCEOF: {
                    int idx = bytes.getUnsignedShort(offset + 1);
                    Constant cst = pool.get(idx);
                    visitor.visitConstant(opcode, offset, 3, cst, 0);
                    return 3;
                }
                case ByteOps.INVOKEINTERFACE: {
                    int idx = bytes.getUnsignedShort(offset + 1);
                    int count = bytes.getUnsignedByte(offset + 3);
                    int expectZero = bytes.getUnsignedByte(offset + 4);
                    Constant cst = pool.get(idx);
                    visitor.visitConstant(opcode, offset, 5, cst,
                                          count | (expectZero << 8));
                    return 5;
                }
                case ByteOps.INVOKEDYNAMIC: {
                  throw new ParseException("invokedynamic not supported");
                }
                case ByteOps.NEWARRAY: {
                    return parseNewarray(offset, visitor);
                }
                case ByteOps.WIDE: {
                    return parseWide(offset, visitor);
                }
                case ByteOps.MULTIANEWARRAY: {
                    int idx = bytes.getUnsignedShort(offset + 1);
                    int dimensions = bytes.getUnsignedByte(offset + 3);
                    Constant cst = pool.get(idx);
                    visitor.visitConstant(opcode, offset, 4, cst, dimensions);
                    return 4;
                }
                case ByteOps.GOTO_W:
                case ByteOps.JSR_W: {
                    int target = offset + bytes.getInt(offset + 1);
                    int newop =
                        (opcode == ByteOps.GOTO_W) ? ByteOps.GOTO :
                        ByteOps.JSR;
                    visitor.visitBranch(newop, offset, 5, target);
                    return 5;
                }
                default: {
                    visitor.visitInvalid(opcode, offset, 1);
                    return 1;
                }
            }
        } catch (SimException ex) {
            ex.addContext("...at bytecode offset " + Hex.u4(offset));
            throw ex;
        } catch (RuntimeException ex) {
            SimException se = new SimException(ex);
            se.addContext("...at bytecode offset " + Hex.u4(offset));
            throw se;
        }
    }

    /**
     * Helper to deal with {@code tableswitch}.
     *
     * @param offset the offset to the {@code tableswitch} opcode itself
     * @param visitor {@code non-null;} visitor to use
     * @return instruction length, in bytes
     */
    private int parseTableswitch(int offset, Visitor visitor) {
        int at = (offset + 4) & ~3; // "at" skips the padding.

        // Collect the padding.
        int padding = 0;
        for (int i = offset + 1; i < at; i++) {
            padding = (padding << 8) | bytes.getUnsignedByte(i);
        }

        int defaultTarget = offset + bytes.getInt(at);
        int low = bytes.getInt(at + 4);
        int high = bytes.getInt(at + 8);
        int count = high - low + 1;
        at += 12;

        if (low > high) {
            throw new SimException("low / high inversion");
        }

        SwitchList cases = new SwitchList(count);
        for (int i = 0; i < count; i++) {
            int target = offset + bytes.getInt(at);
            at += 4;
            cases.add(low + i, target);
        }
        cases.setDefaultTarget(defaultTarget);
        cases.removeSuperfluousDefaults();
        cases.setImmutable();

        int length = at - offset;
        visitor.visitSwitch(ByteOps.LOOKUPSWITCH, offset, length, cases,
                            padding);

        return length;
    }

    /**
     * Helper to deal with {@code lookupswitch}.
     *
     * @param offset the offset to the {@code lookupswitch} opcode itself
     * @param visitor {@code non-null;} visitor to use
     * @return instruction length, in bytes
     */
    private int parseLookupswitch(int offset, Visitor visitor) {
        int at = (offset + 4) & ~3; // "at" skips the padding.

        // Collect the padding.
        int padding = 0;
        for (int i = offset + 1; i < at; i++) {
            padding = (padding << 8) | bytes.getUnsignedByte(i);
        }

        int defaultTarget = offset + bytes.getInt(at);
        int npairs = bytes.getInt(at + 4);
        at += 8;

        SwitchList cases = new SwitchList(npairs);
        for (int i = 0; i < npairs; i++) {
            int match = bytes.getInt(at);
            int target = offset + bytes.getInt(at + 4);
            at += 8;
            cases.add(match, target);
        }
        cases.setDefaultTarget(defaultTarget);
        cases.removeSuperfluousDefaults();
        cases.setImmutable();

        int length = at - offset;
        visitor.visitSwitch(ByteOps.LOOKUPSWITCH, offset, length, cases,
                            padding);

        return length;
    }

    /**
     * Helper to deal with {@code newarray}.
     *
     * @param offset the offset to the {@code newarray} opcode itself
     * @param visitor {@code non-null;} visitor to use
     * @return instruction length, in bytes
     */
    private int parseNewarray(int offset, Visitor visitor) {
        int value = bytes.getUnsignedByte(offset + 1);
        CstType type;
        switch (value) {
            case ByteOps.NEWARRAY_BOOLEAN: {
                type = CstType.BOOLEAN_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_CHAR: {
                type = CstType.CHAR_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_DOUBLE: {
                type = CstType.DOUBLE_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_FLOAT: {
                type = CstType.FLOAT_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_BYTE: {
                type = CstType.BYTE_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_SHORT: {
                type = CstType.SHORT_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_INT: {
                type = CstType.INT_ARRAY;
                break;
            }
            case ByteOps.NEWARRAY_LONG: {
                type = CstType.LONG_ARRAY;
                break;
            }
            default: {
                throw new SimException("bad newarray code " +
                        Hex.u1(value));
            }
        }

        // Revisit the previous bytecode to find out the length of the array
        int previousOffset = visitor.getPreviousOffset();
        ConstantParserVisitor constantVisitor = new ConstantParserVisitor();
        int arrayLength = 0;

        /*
         * For visitors that don't record the previous offset, -1 will be
         * seen here
         */
        if (previousOffset >= 0) {
            parseInstruction(previousOffset, constantVisitor);
            if (constantVisitor.cst instanceof CstInteger &&
                    constantVisitor.length + previousOffset == offset) {
                arrayLength = constantVisitor.value;

            }
        }

        /*
         * Try to match the array initialization idiom. For example, if the
         * subsequent code is initializing an int array, we are expecting the
         * following pattern repeatedly:
         *  dup
         *  push index
         *  push value
         *  *astore
         *
         * where the index value will be incrimented sequentially from 0 up.
         */
        int nInit = 0;
        int curOffset = offset+2;
        int lastOffset = curOffset;
        ArrayList<Constant> initVals = new ArrayList<Constant>();

        if (arrayLength != 0) {
            while (true) {
                boolean punt = false;

                // First, check if the next bytecode is dup.
                int nextByte = bytes.getUnsignedByte(curOffset++);
                if (nextByte != ByteOps.DUP)
                    break;

                /*
                 * Next, check if the expected array index is pushed to
                 * the stack.
                 */
                parseInstruction(curOffset, constantVisitor);
                if (constantVisitor.length == 0 ||
                        !(constantVisitor.cst instanceof CstInteger) ||
                        constantVisitor.value != nInit)
                    break;

                // Next, fetch the init value and record it.
                curOffset += constantVisitor.length;

                /*
                 * Next, find out what kind of constant is pushed onto
                 * the stack.
                 */
                parseInstruction(curOffset, constantVisitor);
                if (constantVisitor.length == 0 ||
                        !(constantVisitor.cst instanceof CstLiteralBits))
                    break;

                curOffset += constantVisitor.length;
                initVals.add(constantVisitor.cst);

                nextByte = bytes.getUnsignedByte(curOffset++);
                // Now, check if the value is stored to the array properly.
                switch (value) {
                    case ByteOps.NEWARRAY_BYTE:
                    case ByteOps.NEWARRAY_BOOLEAN: {
                        if (nextByte != ByteOps.BASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    case ByteOps.NEWARRAY_CHAR: {
                        if (nextByte != ByteOps.CASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    case ByteOps.NEWARRAY_DOUBLE: {
                        if (nextByte != ByteOps.DASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    case ByteOps.NEWARRAY_FLOAT: {
                        if (nextByte != ByteOps.FASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    case ByteOps.NEWARRAY_SHORT: {
                        if (nextByte != ByteOps.SASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    case ByteOps.NEWARRAY_INT: {
                        if (nextByte != ByteOps.IASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    case ByteOps.NEWARRAY_LONG: {
                        if (nextByte != ByteOps.LASTORE) {
                            punt = true;
                        }
                        break;
                    }
                    default:
                        punt = true;
                        break;
                }
                if (punt) {
                    break;
                }
                lastOffset = curOffset;
                nInit++;
            }
        }

        /*
         * For singleton arrays it is still more economical to
         * generate the aput.
         */
        if (nInit < 2 || nInit != arrayLength) {
            visitor.visitNewarray(offset, 2, type, null);
            return 2;
        } else {
            visitor.visitNewarray(offset, lastOffset - offset, type, initVals);
            return lastOffset - offset;
        }
     }


    /**
     * Helper to deal with {@code wide}.
     *
     * @param offset the offset to the {@code wide} opcode itself
     * @param visitor {@code non-null;} visitor to use
     * @return instruction length, in bytes
     */
    private int parseWide(int offset, Visitor visitor) {
        int opcode = bytes.getUnsignedByte(offset + 1);
        int idx = bytes.getUnsignedShort(offset + 2);
        switch (opcode) {
            case ByteOps.ILOAD: {
                visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx,
                                   Type.INT, 0);
                return 4;
            }
            case ByteOps.LLOAD: {
                visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx,
                                   Type.LONG, 0);
                return 4;
            }
            case ByteOps.FLOAD: {
                visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx,
                                   Type.FLOAT, 0);
                return 4;
            }
            case ByteOps.DLOAD: {
                visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx,
                                   Type.DOUBLE, 0);
                return 4;
            }
            case ByteOps.ALOAD: {
                visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx,
                                   Type.OBJECT, 0);
                return 4;
            }
            case ByteOps.ISTORE: {
                visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx,
                                   Type.INT, 0);
                return 4;
            }
            case ByteOps.LSTORE: {
                visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx,
                                   Type.LONG, 0);
                return 4;
            }
            case ByteOps.FSTORE: {
                visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx,
                                   Type.FLOAT, 0);
                return 4;
            }
            case ByteOps.DSTORE: {
                visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx,
                                   Type.DOUBLE, 0);
                return 4;
            }
            case ByteOps.ASTORE: {
                visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx,
                                   Type.OBJECT, 0);
                return 4;
            }
            case ByteOps.RET: {
                visitor.visitLocal(opcode, offset, 4, idx,
                                   Type.RETURN_ADDRESS, 0);
                return 4;
            }
            case ByteOps.IINC: {
                int value = bytes.getShort(offset + 4);
                visitor.visitLocal(opcode, offset, 6, idx,
                                   Type.INT, value);
                return 6;
            }
            default: {
                visitor.visitInvalid(ByteOps.WIDE, offset, 1);
                return 1;
            }
        }
    }

    /**
     * Instruction visitor interface.
     */
    public interface Visitor {
        /**
         * Visits an invalid instruction.
         *
         * @param opcode the opcode
         * @param offset offset to the instruction
         * @param length length of the instruction, in bytes
         */
        public void visitInvalid(int opcode, int offset, int length);

        /**
         * Visits an instruction which has no inline arguments
         * (implicit or explicit).
         *
         * @param opcode the opcode
         * @param offset offset to the instruction
         * @param length length of the instruction, in bytes
         * @param type {@code non-null;} type the instruction operates on
         */
        public void visitNoArgs(int opcode, int offset, int length,
                Type type);

        /**
         * Visits an instruction which has a local variable index argument.
         *
         * @param opcode the opcode
         * @param offset offset to the instruction
         * @param length length of the instruction, in bytes
         * @param idx the local variable index
         * @param type {@code non-null;} the type of the accessed value
         * @param value additional literal integer argument, if salient (i.e.,
         * for {@code iinc})
         */
        public void visitLocal(int opcode, int offset, int length,
                int idx, Type type, int value);

        /**
         * Visits an instruction which has a (possibly synthetic)
         * constant argument, and possibly also an
         * additional literal integer argument. In the case of
         * {@code multianewarray}, the argument is the count of
         * dimensions. In the case of {@code invokeinterface},
         * the argument is the parameter count or'ed with the
         * should-be-zero value left-shifted by 8. In the case of entries
         * of type {@code int}, the {@code value} field always
         * holds the raw value (for convenience of clients).
         *
         * <p><b>Note:</b> In order to avoid giving it a barely-useful
         * visitor all its own, {@code newarray} also uses this
         * form, passing {@code value} as the array type code and
         * {@code cst} as a {@link CstType} instance
         * corresponding to the array type.</p>
         *
         * @param opcode the opcode
         * @param offset offset to the instruction
         * @param length length of the instruction, in bytes
         * @param cst {@code non-null;} the constant
         * @param value additional literal integer argument, if salient
         * (ignore if not)
         */
        public void visitConstant(int opcode, int offset, int length,
                Constant cst, int value);

        /**
         * Visits an instruction which has a branch target argument.
         *
         * @param opcode the opcode
         * @param offset offset to the instruction
         * @param length length of the instruction, in bytes
         * @param target the absolute (not relative) branch target
         */
        public void visitBranch(int opcode, int offset, int length,
                int target);

        /**
         * Visits a switch instruction.
         *
         * @param opcode the opcode
         * @param offset offset to the instruction
         * @param length length of the instruction, in bytes
         * @param cases {@code non-null;} list of (value, target)
         * pairs, plus the default target
         * @param padding the bytes found in the padding area (if any),
         * packed
         */
        public void visitSwitch(int opcode, int offset, int length,
                SwitchList cases, int padding);

        /**
         * Visits a newarray instruction.
         *
         * @param offset   offset to the instruction
         * @param length   length of the instruction, in bytes
         * @param type {@code non-null;} the type of the array
         * @param initVals {@code non-null;} list of bytecode offsets
         * for init values
         */
        public void visitNewarray(int offset, int length, CstType type,
                ArrayList<Constant> initVals);

        /**
         * Set previous bytecode offset
         * @param offset    offset of the previous fully parsed bytecode
         */
        public void setPreviousOffset(int offset);

        /**
         * Get previous bytecode offset
         * @return return the recored offset of the previous bytecode
         */
        public int getPreviousOffset();
    }

    /**
     * Base implementation of {@link Visitor}, which has empty method
     * bodies for all methods.
     */
    public static class BaseVisitor implements Visitor {

        /** offset of the previously parsed bytecode */
        private int previousOffset;

        BaseVisitor() {
            previousOffset = -1;
        }

        /** {@inheritDoc} */
        public void visitInvalid(int opcode, int offset, int length) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void visitNoArgs(int opcode, int offset, int length,
                Type type) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void visitLocal(int opcode, int offset, int length,
                int idx, Type type, int value) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void visitConstant(int opcode, int offset, int length,
                Constant cst, int value) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void visitBranch(int opcode, int offset, int length,
                int target) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void visitSwitch(int opcode, int offset, int length,
                SwitchList cases, int padding) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void visitNewarray(int offset, int length, CstType type,
                ArrayList<Constant> initValues) {
            // This space intentionally left blank.
        }

        /** {@inheritDoc} */
        public void setPreviousOffset(int offset) {
            previousOffset = offset;
        }

        /** {@inheritDoc} */
        public int getPreviousOffset() {
            return previousOffset;
        }
    }

    /**
     * Implementation of {@link Visitor}, which just pays attention
     * to constant values.
     */
    class ConstantParserVisitor extends BaseVisitor {
        Constant cst;
        int length;
        int value;

        /** Empty constructor */
        ConstantParserVisitor() {
        }

        private void clear() {
            length = 0;
        }

        /** {@inheritDoc} */
        @Override
        public void visitInvalid(int opcode, int offset, int length) {
            clear();
        }

        /** {@inheritDoc} */
        @Override
        public void visitNoArgs(int opcode, int offset, int length,
                Type type) {
            clear();
        }

        /** {@inheritDoc} */
        @Override
        public void visitLocal(int opcode, int offset, int length,
                int idx, Type type, int value) {
            clear();
        }

        /** {@inheritDoc} */
        @Override
        public void visitConstant(int opcode, int offset, int length,
                Constant cst, int value) {
            this.cst = cst;
            this.length = length;
            this.value = value;
        }

        /** {@inheritDoc} */
        @Override
        public void visitBranch(int opcode, int offset, int length,
                int target) {
            clear();
        }

        /** {@inheritDoc} */
        @Override
        public void visitSwitch(int opcode, int offset, int length,
                SwitchList cases, int padding) {
            clear();
        }

        /** {@inheritDoc} */
        @Override
        public void visitNewarray(int offset, int length, CstType type,
                ArrayList<Constant> initVals) {
            clear();
        }

        /** {@inheritDoc} */
        @Override
        public void setPreviousOffset(int offset) {
            // Intentionally left empty
        }

        /** {@inheritDoc} */
        @Override
        public int getPreviousOffset() {
            // Intentionally left empty
            return -1;
        }
    }
}
