/*
* JBoss, Home of Professional Open Source
* Copyright 2009-10 Red Hat and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
* @authors Andrew Dinn
*/
package org.jboss.byteman.agent.adapter;

import org.jboss.byteman.rule.helper.Helper;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.Method;
import org.jboss.byteman.rule.type.TypeHelper;
import org.jboss.byteman.rule.binding.Bindings;
import org.jboss.byteman.rule.binding.Binding;
import org.jboss.byteman.agent.adapter.cfg.*;
import org.jboss.byteman.agent.Transformer;
import org.jboss.byteman.agent.RuleScript;
import org.jboss.byteman.agent.TransformContext;

import java.util.*;
import java.io.PrintStream;

/**
 * class which provides base functionality extended by all the location-specific method trigger adapters
 */
public class RuleTriggerMethodAdapter extends RuleGeneratorAdapter
{
    RuleTriggerMethodAdapter(MethodVisitor mv, TransformContext transformContext, int access, String name, String descriptor, String signature, String[] exceptions)
    {
        super(mv, transformContext, access, name, descriptor, transformContext.lookupRule(name, descriptor));
        this.signature = signature;
        this.exceptions = exceptions;
        this.callArrayBindings = new ArrayList<Binding>();
        this.returnBindingType = Type.getReturnType(descriptor);
        this.argumentTypes = Type.getArgumentTypes(descriptor);
        this.argLocalIndices = new int[argumentTypes.length];
        this.bindReturnOrThrowableValue = false;
        this.bindInvokeParams = false;
        this.bindingIndicesSet = false;
    }

    /**
     * method overridden by AT INVOKE method adapter allowing types for the invoked method owner,
     * parameters and return value to be identified. this default version should never get invoked
     * @return an array containing the types of the invoked method owner, parameters and return value
     */
    public Type[] getInvokedTypes()    {
        throw new RuntimeException("RuleTriggerMethodAdapter.getInvokedTypes() : should never get called!");
    }

    /**
     * method overridden by AT NEW method trigger adapter allowing String value for NEWCLASS binding to
     * be retrieved.,
     * this default version should never get invoked
     * @return String value for NEWCLASS binding
     */
    public String getNewClassName()    {
        throw new RuntimeException("RuleTriggerMethodAdapter.getNewClassName() : should never get called!");
    }

    /**
     * method overridden by AT INVOKE method adapter allowing the type of the $! binding to be identified.
     * this default version should only get invoked for an AT EXIT rule where it returns the trigger method
     * return type. the overridden version should only get invoked for an AFTER INVOKE rule where it returns the
     * invoked method return type
     * @return the appropriate return type
     */
    public Type getReturnBindingType()
    {
        return returnBindingType;
    }

    private void setBindingIndices()
    {
        if (bindingIndicesSet) {
            return;
        }

        Bindings bindings = rule.getBindings();
        Iterator<Binding> iterator = bindings.iterator();
        List<Binding> aliasBindings = new ArrayList<Binding>();
        int argLocalIndex = 0;

        // if this is an instance method then the local 0 holds this and args start at local index 1

        if ((access & Opcodes.ACC_STATIC) == 0) {
            argLocalIndex += 1;
        }

        // compute the local indices of each method parameter

        for (int i = 0; i < argumentTypes.length; i++) {
            argLocalIndices[i] = argLocalIndex;
            argLocalIndex += argumentTypes[i].getSize();
        }

        // check the local var bindings and, if any of them are actually refereces to method params
        // alias them to the corresponding param binding

        while (iterator.hasNext()) {
            Binding binding = iterator.next();
            if  (binding.isLocalVar()){
                int localIdx = binding.getLocalIndex();
                if (localIdx < argLocalIndex) {
                    binding = alias(binding, bindings, localIdx);
                    if (binding != null) {
                        // the aliased param binding was not present so ensure it gets added to the
                        // binding set once we have finished iterating. there is no need to add it
                        // to the call bindings since we pass the aliased method param instead
                        aliasBindings.add(binding);
                    }
                } else {
                    callArrayBindings.add(binding);
                }
            }
        }

        bindings.addBindings(aliasBindings);
        
        // now iterate over the param vars and ensure they go into the call bindings list

        iterator = bindings.iterator();
        
        while (iterator.hasNext()) {
            Binding binding = iterator.next();
            if (binding.isParam()) {
                callArrayBindings.add(binding);
            } else if (binding.isReturn()) {
                // in order to be able to add the return value to the args array
                // we have to add a local var to store the value so track that requirement
                bindReturnOrThrowableValue = true;
                saveValueType = getReturnBindingType();
                binding.setDescriptor(saveValueType.getClassName());
                callArrayBindings.add(binding);
            } else if (binding.isThrowable()) {
                // in order to be able to add the return value or throwable value to the args array
                // we have to add a local var to store the value so track that requirement
                bindReturnOrThrowableValue = true;
                // TODO -- allow type to be more accurately identified than this
                saveValueType = Type.getType("Ljava/lang/Throwable;");
                callArrayBindings.add(binding);
            } else if (binding.isParamCount() || binding.isParamArray()) {
                callArrayBindings.add(binding);
            } else if (binding.isInvokeParamArray()) {
                // in order to be able to ad th einvoke parameters to the args array we
                // have to add a local var to store the value so track that requirement
                callArrayBindings.add(binding);
                bindInvokeParams = true;
            } else if (binding.isTriggerClass() || binding.isTriggerMethod() || binding.isNewClass()) {
                callArrayBindings.add(binding);
                binding.setDescriptor("java.lang.String");
            }
        }
        // we don't have to do this but it makes debugging easier
        
        if (true) {
            // ok now sort the callArrayBindings for later use
            // we sort param bindings before local bindings before the param count
            // and param array before the throwable or return binding
            // within param bindings we sort by param idx
            // within local bindings we sort by local var idx

            Comparator<Binding> comparator = new Comparator<Binding>() {
                public int compare(Binding b1, Binding b2)
                {
                    if (b1.isParam()) {
                        if (b2.isParam()) {
                            // sort by param index
                            int i1 = b1.getIndex();
                            int i2 = b2.getIndex();
                            return (i1 < i2 ? -1 : (i1 > i2 ? 1 : 0));
                        } else {
                            // param bindings precede all other types
                            return -1;
                        }
                    } else if (b1.isLocalVar()){
                        if (b2.isParam()) {
                            // param bindings precede all other types
                            return 1;
                        } else if (b2.isLocalVar()) {
                            // sort by local index
                            int i1 = b1.getLocalIndex();
                            int i2 = b2.getLocalIndex();
                            return (i1 < i2 ? -1 : (i1 > i2 ? 1 : 0));
                        } else {
                            // local vars precede all remaining types
                            return -1;
                        }
                    } else if (b1.isParamCount()){
                        if (b2.isParam() || b2.isLocalVar()) {
                            // param and local bindings precede param count
                            return 1;
                        } else if (b2.isParamCount()) {
                            return 0;
                        } else {
                            // param count vars precede all remaining types
                            return -1;
                        }
                    } else if (b1.isParamArray()){
                        if (b2.isParam() || b2.isLocalVar() || b2.isParamCount()) {
                            // param, local and param count bindings precede param array
                            return 1;
                        } else if (b2.isParamArray()) {
                            return 0;
                        } else {
                            // param array vars precede all remaining types
                            return -1;
                        }
                    } else if (b1.isInvokeParamArray()){
                        if (b2.isParam() || b2.isLocalVar() || b2.isParamCount() || b2.isParamArray()) {
                            // param, local and param count and param array bindings precede invoke param array
                            return 1;
                        } else if (b2.isInvokeParamArray()) {
                            return 0;
                        } else {
                            // invoke param array vars precede all remaining types
                            return -1;
                        }
                    } else if (b1.isTriggerClass()) {
                        if (b2.isParam() || b2.isLocalVar() || b2.isParamCount() || b2.isParamArray() || b2.isInvokeParamArray()) {
                            // param, local and param count, param array and invoke param array bindings precede trigger class
                            return 1;
                        } else if (b2.isTriggerClass()) {
                            return 0;
                        } else {
                            return -1;
                        }
                    } else if (b1.isTriggerMethod()) {
                        if(b2.isParam() || b2.isLocalVar() || b2.isParamCount() || b2.isParamArray() || b2.isInvokeParamArray() || b2.isTriggerClass()) {
                            // param, local and param count, param array, invoke param array and trigger class bindings precede trigger method
                            return 1;
                        } else if(b2.isTriggerMethod()) {
                            return 0;
                        } else {
                            return -1;
                        }
                    } else if (b1.isNewClass()) {
                        if(b2.isParam() || b2.isLocalVar() || b2.isParamCount() || b2.isParamArray() || b2.isInvokeParamArray() || b2.isTriggerClass() || b2.isTriggerMethod()) {
                            // param, local and param count, param array, invoke param array and trigger class and trigger method bindings precede new class
                            return 1;
                        } else if(b2.isNewClass()) {
                            return 0;
                        } else {
                            return -1;
                        }
                    } else {
                        // return var always sorts last
                        return 1;
                    }
                }
            };

            Collections.sort(callArrayBindings, comparator);
        }

        // now give each binding a unique index in the object array
        int n = callArrayBindings.size();
        for (int i = 0; i < n; i++) {
            callArrayBindings.get(i).setCallArrayIndex(i);
        }

        bindingIndicesSet = true;
    }

    private Binding alias(Binding binding, Bindings bindings, int localIdx)
    {
        if (((access & Opcodes.ACC_STATIC) == 0) && (localIdx == 0)) {
            String name = "$0";
            Binding alias = bindings.lookup(name);
            if (alias == null) {
                alias = new Binding(rule, name);
                alias.setDescriptor(binding.getDescriptor());
                alias.setLocalIndex(binding.getLocalIndex());
                binding.aliasTo(alias);
                // alias var was new so return it for addition to the binding set
                return alias;
            } else {
                // just alias the binding
                binding.aliasTo(alias);
                return null;
            }
        }

        for (int i = 0; i < argLocalIndices.length; i++) {
            if (argLocalIndices[i] == localIdx) {
                String name = "$" + (i + 1);
                Binding alias = bindings.lookup(name);
                if (alias == null) {
                    alias = new Binding(rule, name);
                    alias.setDescriptor(binding.getDescriptor());
                    alias.setLocalIndex(binding.getLocalIndex());
                    binding.aliasTo(alias);
                    // alias var was new so return it for addition to the binding set
                    return alias;
                } else {
                    // just alias the binding
                    binding.aliasTo(alias);
                    return null;
                }
            }
        }

        return null;
    }

    private int doReturnOrThrowSave()
    {
        // allocate a slot for the return or throwable on top of the stack
        // stash the value into the slot and return the slot idx

        int saveValueSlot = newLocal(saveValueType);
        if (saveValueType.getSize() == 2) {
            visitInsn(Opcodes.DUP2);
        } else {
            visitInsn(Opcodes.DUP);
        }
        storeLocal(saveValueSlot);

        return saveValueSlot;
    }

    private int doInvokeBindingsSave()
    {
        // allocate a slot for the invoke parameters array,
        // construct the array
        // stash it into the slot and return the slot idx

        Type objectType = Type.getType(Object.class);
        Type objectArrayType = Type.getType("[Ljava/lang/Object;");
        Type[] invokeParamTypes = getInvokedTypes();
        int savedValueCount = invokeParamTypes.length;

        // create the array and save it in a local var slot

        int arrayValueSlot = newLocal(objectArrayType);
        push(savedValueCount);
        newArray(objectType);
        storeLocal(arrayValueSlot);

        // pop the arguments off the stack into the invoke parameters array
        // n.b. the top one is the last parameter

        for (int i = savedValueCount - 1; i >= 0; i--) {
            Type type = invokeParamTypes[i];
            if (type != null) {
                // convert value to object if needed
                box(type);
                // load array and  swap under value
                loadLocal(arrayValueSlot);
                swap(objectArrayType, objectType);
                // load index and swap under value
                push(i);
                swap(Type.INT_TYPE, objectType);
            } else {
                // this is a static method and index is 0 so we install null in the array
                // load array index and then null
                loadLocal(arrayValueSlot);
                push(i);
                push((Type)null);
            }
            // store the value in the array as an object
            arrayStore(objectType);
        }

        // now restore the arguments from the array in increasing order

        for (int i = 0; i < savedValueCount; i++) {
            Type type = invokeParamTypes[i];
            if (type != null) {
                // load the array, retrieve the object and unbox if needed
                loadLocal(arrayValueSlot);
                push(i);
                arrayLoad(objectType);
                //if (type.getSort() != Type.OBJECT && type.getSort() != Type.ARRAY) {
                    unbox(type);
                //}
            } else {
                // this is a static method so no target instance to stack
            }
        }

        return arrayValueSlot;
    }

    /**
     * stack an argument array containing all the values which need to be bound to parameters or local variables
     * in the rule or a null pointer if no bindings are required. this method also inserts a copy of the array
     * below the initial two top entries if any of the bindings can potentially be updated by the rule, allowing
     * the updated values to be written back on return from the call to the rule engine.
     * @param saveSlot a local variable slot containing either the return value which the trigger method is about
     * to return or the Throwable the method is about to throw. this is only valid if, respectively, the rule
     * location is AT EXIT/AFTER INVOKE or AT THROW and the rule body contains a reference to the return value ($!)
     * or throwable value ($@).
     * @return true if the rule may update the argument array and hence if a copy of the array has been inserted
     * below the initial two top entries otherwise false.
     */
    private boolean doArgLoad(int saveSlot)
    {
        if (callArrayBindings.size() ==  0) {
            push((org.objectweb.asm.Type)null);
            return false;
        }

        // create the array used to pass the bindings

        int arraySize = callArrayBindings.size();

        push(arraySize);
        Type objectType = Type.getType(Object.class);
        newArray(objectType);

        // check if any of the bindings gets updated. if so we need to stash a copy of the bindings array
        // below the key and owner so we can pull out the updated values and update local var/param slots
        // once the call ahs completed

        boolean doUpdates = false;

        for (int i = 0; i < arraySize; i++) {
            Binding binding = callArrayBindings.get(i);
            if (binding.isUpdated()) {
                doUpdates = true;
                break;
            }
        }

        if (doUpdates) {
            // insert copy of array below first two call arguments
            // [.. key owner bindings ] ==> [.. bindings key owner bindings ]
            mv.visitInsn(Opcodes.DUP_X2);
        }

        // now install required values into bindings array
        for (int i = 0; i < arraySize; i++) {
            Binding binding = callArrayBindings.get(i);
            dup();
            push(i);
            if (binding.isParam()) {
                int idx = binding.getIndex() - 1;
                loadArg(idx);
                box(argumentTypes[idx]);
            } else if (binding.isLocalVar()){
                int idx = binding.getLocalIndex();
                loadLocal(idx);
                // local type may be Object when actual type is more specific but we don't care here
                // special case -- if this is a bool we may have recorded it as int when it was saved
                // using an iload. we can use the descriptor in the binding to detect this special case
                if (binding.getDescriptor().equals("boolean")) {
                    box(Type.BOOLEAN_TYPE);
                } else {
                    box(getLocalType(idx));
                }
            } else if (binding.isParamCount()) {
                int count = argumentTypes.length;
                push(count);
                box(Type.INT_TYPE);
            } else if (binding.isParamArray()){
                int count = argumentTypes.length;
                push(count + 1);
                newArray(objectType);
                dup();
                push(0);
                if ((access & Opcodes.ACC_STATIC) == 0) {
                    loadThis();
                } else {
                    push((Type)null);
                }
                arrayStore(objectType);
                for (int idx = 0; idx < count; idx++) {
                    dup();
                    push(idx + 1);
                    loadArg(idx);
                    box(argumentTypes[idx]);
                    arrayStore(objectType);
                }
            } else if (binding.isInvokeParamArray()) {
                loadLocal(saveSlot);
            } else if (binding.isTriggerClass()) {
                String triggerClassName = TypeHelper.internalizeClass(getTriggerClassName());
                visitLdcInsn(triggerClassName);
            } else if (binding.isTriggerMethod()) {
                String triggerMethodName = name + TypeHelper.internalizeDescriptor(descriptor);
                visitLdcInsn(triggerMethodName);
            } else if (binding.isNewClass()) {
                String typeName = getNewClassName();
                visitLdcInsn(typeName);
            } else if (binding.isThrowable() | binding.isReturn()) {
                loadLocal(saveSlot);
                box(saveValueType);
            }
            arrayStore(objectType);
        }

        return doUpdates;
    }

    /**
     * plant code to copy back any updated values from the argument array to the relevant local variable slots
     */
    private void doArgUpdate()
    {
        // at entry the top of the stack contains the object array
        // for an AT EXIT rule the entry below this is the return value

        Type objectType = Type.getType(Object.class);
        int arraySize = callArrayBindings.size();
        int lastUpdated = -1;
        int returnIdx = -1;


        // identify which is the last index we will need to update. n.b. if we find a return value we will
        // update that last

        for (int i = 0; i < arraySize; i++) {
            Binding binding = callArrayBindings.get(i);
            if (binding.isUpdated()) {
                lastUpdated = i;
                if (binding.isReturn()) {
                    returnIdx = i;
                }
            }
        }
        
        // if the return value is updated it gets done last
        if (returnIdx >= 0) {
            lastUpdated = returnIdx;
        }

        // write back all other args then stack new return value and drop old one
        for (int i = 0; i < arraySize; i++) {
            Binding binding = callArrayBindings.get(i);
            if (binding.isUpdated() && !binding.isReturn()) {
                // if this is the last update then we consume the arguments array
                // otherwise we need to copy it
                if (i != lastUpdated) {
                    dup();
                }
                push(i);
                arrayLoad(objectType);
                if (binding.isParam()) {
                    int idx = binding.getIndex() - 1;
                    unbox(argumentTypes[idx]);
                    storeArg(idx);
                } else if (binding.isLocalVar()) {
                    int idx = binding.getLocalIndex();
                    Type t = getLocalType(idx);
                    // cannot always trust locals flagged as Object type because we sometimes
                    // insert that when we see an ALOAD for a local var we do not know about.
                    // but we can rely on the descriptor in the binding which we derived during
                    // rule check processing
                    // similarly, if this is a bool we may have recorded it as int when it was saved
                    // using an iload.
                    if (t.getClassName().equals("java.lang.Object")) {
                        String descriptor = binding.getDescriptor();
                        t = Type.getType("L" + descriptor.replace('.', '/') + ";");
                    } else if (t == Type.INT_TYPE && binding.getDescriptor().equals("boolean")) {
                        t = Type.BOOLEAN_TYPE;
                    }
                    unbox(t);
                    storeLocal(idx);
                }
            }
        }

        // if we had a return value to process then the args array will still be on top of the stack above
        // the old return value

        if (returnIdx >= 0) {
            // get rid of old return value remembering to use the correct type for $!
            // which varies depending if this is an AT EXIT or AFTER CALL rule
            if (saveValueType.getSize() == 2) {
                mv.visitInsn(Opcodes.DUP_X2);
                mv.visitInsn(Opcodes.POP);
                mv.visitInsn(Opcodes.POP2);
            } else {
                mv.visitInsn(Opcodes.SWAP);
                mv.visitInsn(Opcodes.POP);
            }

            push(returnIdx);
            arrayLoad(objectType);
            // use the correct type for $! which varies depending if this is an AT EXIT or AFTER CALL rule
            unbox(saveValueType);
        }
    }
    /**
     * return true if the current block is a handler which catches a byteman exception thrown by the byteman runtime
     * @return true if the current block is a handler false if not
     */
    protected boolean inBytemanHandler()
    {
        return cfg.inBytemanHandler();
    }

    /**
     * return true if the current block is in a trigger block injected by Byteman
     * @return true if the current block is in a trigger block false if not
     */
    protected boolean inBytemanTrigger()
    {
        return cfg.inBytemanTrigger();
    }
    /**
     * return true if the current block is handler which catches a thrown exception within the scope
     * of a monitor enter in order to be able exit the monitor and rethrow the exception
     * @return true if the current block is in a rethrow handler false if not
     */
    protected boolean inRethrowHandler()
    {
        return cfg.inRethrowHandler();
    }

    protected String getMethodName()
    {
        return name + TypeHelper.internalizeDescriptor(descriptor);
    }

    private RuleScript ruleScript;
    private String signature;
    protected String[] exceptions;
    private Type[] argumentTypes;
    private Type saveValueType;
    private int[] argLocalIndices;
    private List<Binding> callArrayBindings;
    private boolean bindReturnOrThrowableValue;
    private boolean bindInvokeParams;
    private boolean bindingIndicesSet;
    private Type returnBindingType;

    private CFG cfg;

    // methdos copied from GeneratorAdapter but modified so they invoke local MethodVisitor
    // method implementations rather than delegating to the next MethodVisitor in line


    // overridden methods from MethodVisitor

    @Override
    public void visitCode()
    {
        super.visitCode();
        // create a control flow graph for the method
        String methodName = getTriggerClassName() + "." + this.name + this.descriptor;
        Label newStart = super.newLabel();
        this.cfg = new CFG(methodName, newStart);
        visitLabel(newStart);
    }

    @Override
    public void visitInsn(int opcode)
    {
        super.visitInsn(opcode);
        // look for interesting instructions which need inserting into the CFG
        switch(opcode)
        {
            case Opcodes.IRETURN:
            case Opcodes.LRETURN:
            case Opcodes.FRETURN:
            case Opcodes.DRETURN:
            case Opcodes.ARETURN:
            case Opcodes.RETURN:
            case Opcodes.ATHROW:
            {
                // add this instruction to the current block and then start a new current block
                cfg.add(opcode);
                Label newStart = super.newLabel();
                // must call split before visiting the label
                cfg.split(newStart);
                visitLabel(newStart);
            }
            break;
            case Opcodes.MONITORENTER:
            case Opcodes.MONITOREXIT:
            {
                // just add this instruction to the current block
                cfg.add(opcode);
            }
            break;
            default:
            {
                cfg.add(opcode);
            }
        }
    }

    @Override
    public void visitIincInsn(int var, int increment)
    {
        super.visitIincInsn(var, increment);
        cfg.add(Opcodes.IINC, var, increment);
    }

    @Override
    public void visitIntInsn(int opcode, int operand)
    {
        super.visitIntInsn(opcode, operand);
        cfg.add(opcode, operand);
    }

    @Override
    public void visitLdcInsn(Object cst)
    {
        super.visitLdcInsn(cst);
        cfg.add(Opcodes.LDC, cst.toString());
    }

    @Override
    public void visitVarInsn(int opcode, int var)
    {
        super.visitVarInsn(opcode, var);
        cfg.add(opcode, var);
    }

    @Override
    public void visitTypeInsn(int opcode, String desc)
    {
        super.visitTypeInsn(opcode, desc);
        cfg.add(opcode, desc);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc)
    {
        super.visitFieldInsn(opcode, owner, name, desc);
        cfg.add(opcode, owner, name, desc);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)
    {
        super.visitMethodInsn(opcode, owner, name, desc, itf);
        cfg.add(opcode, owner, name, desc, itf);
    }

    @Override
    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
        super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
        cfg.add(Opcodes.INVOKEDYNAMIC, name, desc);
    }

    @Override
    public void visitJumpInsn(int opcode, Label label)
    {
        super.visitJumpInsn(opcode, label);
        switch (opcode)
        {
            case Opcodes.IFEQ:
            case Opcodes.IFNE:
            case Opcodes.IFLT:
            case Opcodes.IFGE:
            case Opcodes.IFGT:
            case Opcodes.IFLE:
            case Opcodes.IF_ICMPEQ:
            case Opcodes.IF_ICMPNE:
            case Opcodes.IF_ICMPLT:
            case Opcodes.IF_ICMPGE:
            case Opcodes.IF_ICMPGT:
            case Opcodes.IF_ICMPLE:
            case Opcodes.IF_ACMPEQ:
            case Opcodes.IF_ACMPNE:
            {
                // create a new current block and add the label supplied in the call as the
                // first out of the old current block and the label of the new current block as
                // the second out
                cfg.add(opcode);
                Label newStart = super.newLabel();
                // must call split before visiting the label
                cfg.split(newStart, label, newStart);
                visitLabel(newStart);
            }
            break;
            case Opcodes.GOTO:
            {
                // create a new current block and  add the label supplied in the call as the
                // first out of the old current block
                cfg.add(opcode);
                Label newStart = super.newLabel();
                // must call split before visiting the label
                cfg.split(newStart, label);
                visitLabel(newStart);
            }
            break;
            case Opcodes.JSR:
            {
                // create a new current block and add the label supplied in the call as the first out
                // of the current block -- the new current block is a potential return point from the
                // JSR but we cannot represent that statically
                cfg.add(opcode);
                Label newStart = super.newLabel();
                // must call split before visiting the label
                cfg.split(newStart, label, newStart);
                visitLabel(newStart);
            }
            break;
            case Opcodes.IFNULL:
            case Opcodes.IFNONNULL:
            {
                // create a new current block and add the label supplied in the call as the
                // first out of the old current block and the label of the new current block as
                // the second out
                cfg.add(opcode);
                Label newStart = super.newLabel();
                // must call split before visiting the label
                cfg.split(newStart, label, newStart);
                visitLabel(newStart);
            }
            break;
        }
    }

    @Override
    public void visitLabel(Label label)
    {
        super.visitLabel(label);

        // tell the CFG to visit this label

        cfg.visitLabel(label);

        // if this is a try catch block end then we need to visit the try catch block now
        // this ensures that try catch blocks which enclose trigger blocks are listed after the
        //try catch blocks

        if (cfg.tryCatchEnd(label)) {
            for (TryCatchDetails details : cfg.tryCatchEndDetails(label)) {
                super.visitTryCatchBlock(details.getStart(), details.getEnd(), details.getHandler(), details.getType());
            }
        }
    }

    public void visitTriggerStart(Label label)
    {
        // tell the CFG to visit this label

        cfg.visitTriggerStart(label);

        visitLabel(label);
    }

    public void visitTriggerEnd(Label label)
    {
        // tell the CFG to visit this label

        cfg.visitTriggerEnd(label);

        visitLabel(label);

        // ok, update the trigger details with labels for the handler blocks we are going to generate

        TriggerDetails details = cfg.triggerEndDetails(label);

        // we always add an execute handler
        // we don't need a return or throw handler if we are in the scope of a monitor enter/exit pair
        // all the handler regions end here so we use the same end label for all of them
        Label end = new Label();
        Label executeHandler = new Label();

        if (!cfg.inOpenMonitor()) {
            // ok this one needs the full set of handlers and the subtype handlers need to come first
            Label returnHandler = newLabel();
            details.setEarlyReturnHandler(returnHandler);
            visitTryCatchBlock(details.getStart(), end, returnHandler, CFG.EARLY_RETURN_EXCEPTION_TYPE_NAME);
            Label throwHandler = newLabel();
            details.setThrowHandler(throwHandler);
            visitTryCatchBlock(details.getStart(), end, throwHandler, CFG.THROW_EXCEPTION_TYPE_NAME);
        }

        details.setExecuteHandler(executeHandler);
        visitTryCatchBlock(details.getStart(), end, executeHandler, CFG.EXECUTE_EXCEPTION_TYPE_NAME);

        // ok this fixes the  handler end label at the same pont as the trigger end label
        
        visitLabel(end);
    }

    /*
    @Override
    public void visitLdcInsn(Object cst) {
        super.visitLdcInsn(cst);
    }
    @Override
    public void visitIincInsn(int var, int increment) {
        super.visitIincInsn(var, increment);
    }
    */

    @Override
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels)
    {
        super.visitTableSwitchInsn(min, max, dflt, labels);
        cfg.add(Opcodes.TABLESWITCH, min, max);
        // create a new current block and then add the default lable and each of the switch labels as an
        // outgoing path from the current block
        Label newStart = super.newLabel();
        // must call split before visiting the label
        cfg.split(newStart, dflt, labels);
        visitLabel(newStart);
    }

    @Override
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)
    {
        super.visitLookupSwitchInsn(dflt, keys, labels);
        cfg.add(Opcodes.TABLESWITCH, keys);
        // create a new current block and then add the default lable and each of the switch labels as an
        // outgoing path from the current block
        Label newStart = super.newLabel();
        // must call split before visiting the label
        cfg.split(newStart, dflt, labels);
        visitLabel(newStart);
    }

    @Override
    public void visitMultiANewArrayInsn(String desc, int dims)
    {
        super.visitMultiANewArrayInsn(desc, dims);
        cfg.add(Opcodes.MULTIANEWARRAY, desc, dims);
    }

    @Override
    public void visitTryCatchBlock(Label start, Label end, Label handler, String type)
    {
        // don't forward details to the next visitor in line until we reach the end label
        //this ensures we can insert trigger catch blocks in between the existing handlers
        // super.visitTryCatchBlock(start, end, handler, type);

        // now tell the cfg to visit this block

        cfg.visitTryCatchBlock(start, end, handler, type);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals)
    {
        Type returnType =  Type.getReturnType(descriptor);

        // check whether there are outstanding monitor opens at the start of the trigger
        // block and, if so, insert a handler which unlocks the monitor and then rethrows
        // the exception.

        Iterator<TriggerDetails> iterator = cfg.triggerDetails();
        boolean noneLeft = true;

        while (iterator.hasNext()) {
            TriggerDetails details = iterator.next();
            // see if this trigger has any open monitor enters which will need closing
            Iterator<CodeLocation> openEnters = cfg.getOpenMonitors(details);
            if (openEnters.hasNext()) {
                // add a handler here which unlocks each object and rethrows the
                // saved exception then protect it with a try catch block and update
                // the trigger details so that it is the target of this block

                // if we get here the return and throw handlers labels should be null
                if (details.getEarlyReturnHandler() != null || details.getThrowHandler() != null) {
                    Helper.err("unexpected : trigger region with open monitorenters has subtype handler!");
                }

                // generate rethrow code and mark the handler as a try catch block for all
                // three exception types
                Label newStart = newLabel();
                Label newEnd = newLabel();
                Label newEarlyReturn = newLabel();
                Label newThrow = newLabel();
                Label newExecute = newLabel();
                visitLabel(details.getExecuteHandler());
                visitLabel(newStart);
                while (openEnters.hasNext()) {
                    CodeLocation enterLocation = openEnters.next();
                    int varIdx = cfg.getSavedMonitorIdx(enterLocation);
                    // call super method to avoid indexing these instructions
                    visitVarInsn(Opcodes.ALOAD, varIdx);
                    visitInsn(Opcodes.MONITOREXIT);
                }
                visitInsn(Opcodes.ATHROW);
                // add try catch blocks for the 3 exception types
                visitTryCatchBlock(newStart, newEnd, newEarlyReturn, CFG.EARLY_RETURN_EXCEPTION_TYPE_NAME);
                visitTryCatchBlock(newStart, newEnd, newThrow, CFG.THROW_EXCEPTION_TYPE_NAME);
                visitTryCatchBlock(newStart, newEnd, newExecute, CFG.EXECUTE_EXCEPTION_TYPE_NAME);
                // visit the end of the handler blocks so they gets processed
                visitLabel(newEnd);
                // update the details so it tracks these new ecetpion regions
                details.setThrowHandler(newThrow);
                details.setEarlyReturnHandler(newEarlyReturn);
                details.setExecuteHandler(newExecute);
            }
        }

        // ok, so now we have to add the handler code for trigger block try catch handlers
        // we only need to add the handler code once but we need to make sure it is the target of
        //  all the try catch blocks by visiting their handler label before we insert the code

        iterator = cfg.triggerDetails();

        while (iterator.hasNext()) {
            TriggerDetails details = iterator.next();
            visitLabel(details.getEarlyReturnHandler());
        }

        if (Transformer.isVerbose()) {
            getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
            visitLdcInsn("caught ReturnException");
            invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println(String)"));
        }
        // add exception handling code subclass first
        if (returnType == Type.VOID_TYPE) {
            // drop exception and just return
            pop();
            visitInsn(Opcodes.RETURN);
        } else {
            // fetch value from exception, unbox if needed and return value
            Method getReturnValueMethod = Method.getMethod("Object getReturnValue()");
            invokeVirtual(CFG.EARLY_RETURN_EXCEPTION_TYPE, getReturnValueMethod);
            unbox(returnType);
            returnValue();
        }

        iterator = cfg.triggerDetails();

        while (iterator.hasNext()) {
            TriggerDetails details = iterator.next();
            visitLabel(details.getThrowHandler());
        }

        if (Transformer.isVerbose()) {
            getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
            visitLdcInsn("caught ThrowException");
            invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println(String)"));
        }
        // fetch value from exception, unbox if needed and return value
        Method getThrowableMethod = Method.getMethod("Throwable getThrowable()");
        invokeVirtual(CFG.THROW_EXCEPTION_TYPE, getThrowableMethod);
        throwException();

        // execute exception  comes last because it is the super of the othher two classes
        
        iterator = cfg.triggerDetails();

        while (iterator.hasNext()) {
            TriggerDetails details = iterator.next();
            visitLabel(details.getExecuteHandler());
        }

        if (Transformer.isVerbose()) {
            getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
            visitLdcInsn("caught ExecuteException");
            invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println(String)"));
        }
        // rethrow an execute exception
        throwException();

        super.visitMaxs(maxStack, maxLocals);

        // hmm, don't think we need this
        cfg.visitMaxs();
    }

    @Override
    public void visitEnd()
    {
        super.visitEnd();
        // trash the current label
        cfg.visitEnd();
    }

    /**
         * inject the rule trigger code
     */
    protected void injectTriggerPoint()
    {
        // we need to set this here to avoid recursive re-entry into inject routine

        rule.setTypeInfo(getTriggerClassName(), access, name, descriptor, exceptions);
        String key = rule.getKey();
        Type ruleType = Type.getType(TypeHelper.externalizeType("org.jboss.byteman.rule.Rule"));
        Method method = Method.getMethod("void execute(String, Object, Object[])");
        // we are at the relevant line in the method -- so add a trigger call here

        Helper.verbose("RuleTriggerMethodAdapter.injectTriggerPoint : inserting trigger into " + getTriggerClassName() + "." + getMethodName() + " for rule " + rule.getName());

        Label startLabel = newLabel();
        Label endLabel = newLabel();
        visitTriggerStart(startLabel);

        // ensure binding indices have been installed
        
        setBindingIndices();

        // a local var slot to store a value for $! (AT RETURN), $^ (AT THROW) or $@ (AT INVOKE)
        // note that only one of these can appear in any given rule so we only need one slot

        int saveValueSlot;

        if (bindReturnOrThrowableValue) {
            saveValueSlot = doReturnOrThrowSave();
        } else if (bindInvokeParams) {
            saveValueSlot = doInvokeBindingsSave();
        } else {
            saveValueSlot = -1;
        }
        push(key);
        if ((access & Opcodes.ACC_STATIC) == 0) {
            loadThis();
        } else {
            push((Type)null);
        }
        boolean handleUpdates;
        handleUpdates = doArgLoad(saveValueSlot);
        // free the local slot if we need to
        if (saveValueSlot >= 0) {
            popLocal(saveValueSlot);
        }
        invokeStatic(ruleType, method);
        // if the rule can modify local variables then generate code to perform the update
        if (handleUpdates) {
            doArgUpdate();
        }
        visitTriggerEnd(endLabel);
    }
}
