/*
 * Copyright 2016 NAVER Corp.
 *
 * 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.navercorp.pinpoint.profiler.instrument;

import com.navercorp.pinpoint.bootstrap.interceptor.Interceptor;
import com.navercorp.pinpoint.bootstrap.interceptor.registry.InterceptorRegistry;
import com.navercorp.pinpoint.common.profiler.util.IntegerUtils;
import com.navercorp.pinpoint.profiler.instrument.interceptor.InterceptorDefinition;
import com.navercorp.pinpoint.profiler.instrument.interceptor.InterceptorType;
import com.navercorp.pinpoint.profiler.util.JavaAssistUtils;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @author jaehong.kim
 */
public class ASMMethodVariables {
    private static final Type BYTE_TYPE = Type.getObjectType("java/lang/Byte");

    private static final Type BOOLEAN_TYPE = Type.getObjectType("java/lang/Boolean");

    private static final Type SHORT_TYPE = Type.getObjectType("java/lang/Short");

    private static final Type CHARACTER_TYPE = Type.getObjectType("java/lang/Character");

    private static final Type INTEGER_TYPE = Type.getObjectType("java/lang/Integer");

    private static final Type FLOAT_TYPE = Type.getObjectType("java/lang/Float");

    private static final Type LONG_TYPE = Type.getObjectType("java/lang/Long");

    private static final Type DOUBLE_TYPE = Type.getObjectType("java/lang/Double");

    private static final Type OBJECT_TYPE = Type.getObjectType("java/lang/Object");

    private final LabelNode interceptorVariableStartLabelNode = new LabelNode();
    private final LabelNode interceptorVariableEndLabelNode = new LabelNode();

    private final String declaringClassInternalName;
    private final MethodNode methodNode;
    private final Type[] argumentTypes;
    private final Type returnType;

    private boolean initializedInterceptorLocalVariables = false;
    private AbstractInsnNode enterInsnNode;
    private AbstractInsnNode exitInsnNode;

    private int nextLocals;

    private int interceptorVarIndex;
    private int arg0VarIndex;
    private int arg1VarIndex;
    private int arg2VarIndex;
    private int arg3VarIndex;
    private int arg4VarIndex;
    private int argsVarIndex;
    private int classNameVarIndex;
    private int methodNameVarIndex;
    private int parameterDescriptionVarIndex;
    private int apiIdVarIndex;

    private int resultVarIndex;
    private int throwableVarIndex;

    public ASMMethodVariables(final String declaringClassInternalName, final MethodNode methodNode) {
        this.declaringClassInternalName = declaringClassInternalName;
        this.methodNode = methodNode;
        this.nextLocals = methodNode.maxLocals;
        this.argumentTypes = Type.getArgumentTypes(methodNode.desc);
        this.returnType = Type.getReturnType(methodNode.desc);
    }

    public AbstractInsnNode getEnterInsnNode() {
        return enterInsnNode;
    }

    public AbstractInsnNode getExitInsnNode() {
        return exitInsnNode;
    }

    public String[] getParameterTypes() {
        final String[] parameterTypes = new String[this.argumentTypes.length];
        for (int i = 0; i < this.argumentTypes.length; i++) {
            parameterTypes[i] = this.argumentTypes[i].getClassName();
        }

        return parameterTypes;
    }

    public String[] getParameterNames() {
        if (this.argumentTypes.length == 0) {
            return new String[0];
        }

        final List<LocalVariableNode> localVariableNodes = this.methodNode.localVariables;
        int localVariableStartIndex = 1;
        if (isStatic()) {
            // static method is none this.
            localVariableStartIndex = 0;
        }

        if (localVariableNodes == null || localVariableNodes.size() <= localVariableStartIndex || (this.argumentTypes.length + localVariableStartIndex) > localVariableNodes.size()) {
            // make simple argument names.
            final String[] names = new String[this.argumentTypes.length];
            for (int i = 0; i < this.argumentTypes.length; i++) {
                final String className = this.argumentTypes[i].getClassName();
                if (className != null) {
                    final int findIndex = className.lastIndexOf('.');
                    if (findIndex == -1) {
                        names[i] = className;
                    } else {
                        names[i] = className.substring(findIndex + 1);
                    }
                } else {
                    names[i] = this.argumentTypes[i].getDescriptor();
                }
            }
            return names;
        }

        // sort by index.
        Collections.sort(localVariableNodes, new Comparator<LocalVariableNode>() {
            @Override
            public int compare(LocalVariableNode o1, LocalVariableNode o2) {
                return IntegerUtils.compare(o1.index, o2.index);
            }
        });
        String[] names = new String[this.argumentTypes.length];

        for (int i = 0; i < this.argumentTypes.length; i++) {
            final String name = localVariableNodes.get(localVariableStartIndex++).name;
            if (name != null) {
                names[i] = name;
            } else {
                names[i] = "";
            }
        }

        return names;
    }

    public String getReturnType() {
        return this.returnType.getClassName();
    }

    public boolean hasInterceptor() {
        final List<LocalVariableNode> localVariableNodes = this.methodNode.localVariables;
        if (localVariableNodes == null) {
            return false;
        }

        for (LocalVariableNode node : localVariableNodes) {
            if (node.name.equals("_$PINPOINT$_interceptor")) {
                return true;
            }
        }

        return false;
    }

    // new method only.
    public void initLocalVariables(final InsnList instructions) {
        // find enter & exit instruction.
        final LabelNode variableStartLabelNode = new LabelNode();
        final LabelNode variableEndLabelNode = new LabelNode();
        if(instructions.getFirst() != null) {
            instructions.insertBefore(instructions.getFirst(), variableStartLabelNode);
        } else {
            instructions.insert(variableStartLabelNode);
        }
        instructions.insert(instructions.getLast(), variableEndLabelNode);

        if (!isStatic()) {
            addLocalVariable("this", Type.getObjectType(this.declaringClassInternalName).getDescriptor(), variableStartLabelNode, variableEndLabelNode);
        }

        for (Type type : this.argumentTypes) {
            addLocalVariable(JavaAssistUtils.javaClassNameToVariableName(type.getClassName()), type.getDescriptor(), variableStartLabelNode, variableEndLabelNode);
        }
    }

    public boolean initInterceptorLocalVariables(final InsnList instructions, final int interceptorId, final InterceptorDefinition interceptorDefinition, final int apiId) {
        if (this.initializedInterceptorLocalVariables) {
            return false;
        }
        this.initializedInterceptorLocalVariables = true;

        // find enter & exit instruction.
        if (isConstructor()) {
            this.enterInsnNode = findInitConstructorInstruction();
        } else {
            this.enterInsnNode = methodNode.instructions.getFirst();
        }

        if (this.enterInsnNode == null) {
            throw new IllegalStateException("not found enter code. " + declaringClassInternalName + "/" + methodNode.name + methodNode.desc);
        }

        this.exitInsnNode = methodNode.instructions.getLast();

        // setup interceptor variables start/end label.
        this.methodNode.instructions.insertBefore(this.enterInsnNode, this.interceptorVariableStartLabelNode);
        this.methodNode.instructions.insert(this.exitInsnNode, this.interceptorVariableEndLabelNode);

        // initialize interceptor variable.
        initInterceptorVar(instructions, interceptorId);

        // initialize argument variable.
        final InterceptorType interceptorType = interceptorDefinition.getInterceptorType();
        if (interceptorType == InterceptorType.ARRAY_ARGS) {
            // Object target, Object[] args
            initArgsVar(instructions);
        } else if (interceptorType == InterceptorType.STATIC) {
            // Object target, String declaringClassInternalName, String methodName, String parameterDescription, Object[] args
            initClassNameVar(instructions);
            initMethodNameVar(instructions);
            initParameterDescriptionVar(instructions);
            initArgsVar(instructions);
        } else if (interceptorType == InterceptorType.API_ID_AWARE) {
            // Object target, int apiId, Object[] args
            initApiIdVar(apiId, instructions);
            initArgsVar(instructions);
        } else if (interceptorType == InterceptorType.BASIC) {
            int interceptorMethodParameterCount = getInterceptorParameterCount(interceptorDefinition);
            final int methodParameterCount = this.argumentTypes.length;
            final int argumentCount = Math.min(methodParameterCount, interceptorMethodParameterCount);
            for (int i = 1; i <= argumentCount; i++) {
                // Object target, Object arg0, Object arg1, Object arg2, Object arg3, Object arg4
                if (i == 1) {
                    initArg0Var(instructions);
                } else if (i == 2) {
                    initArg1Var(instructions);
                } else if (i == 3) {
                    initArg2Var(instructions);
                } else if (i == 4) {
                    initArg3Var(instructions);
                } else if (i == 5) {
                    initArg4Var(instructions);
                }
            }
        }

        return true;
    }

    AbstractInsnNode findInitConstructorInstruction() {
        int nested = 0;
        for (AbstractInsnNode insnNode = this.methodNode.instructions.getFirst(); insnNode != null; insnNode = insnNode.getNext()) {
            if (insnNode instanceof TypeInsnNode) {
                if (insnNode.getOpcode() == Opcodes.NEW) {
                    // new object().
                    nested++;
                }
            } else if (insnNode instanceof MethodInsnNode) {
                final MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode;
                if (methodInsnNode.getOpcode() == Opcodes.INVOKESPECIAL && methodInsnNode.name.equals("<init>")) {
                    if (--nested < 0) {
                        // find this() or super().
                        return insnNode.getNext();
                    }
                }
            }
        }

        return null;
    }


    private void initInterceptorVar(final InsnList instructions, final int interceptorId) {
        assertInitializedInterceptorLocalVariables();
        this.interceptorVarIndex = addInterceptorLocalVariable("_$PINPOINT$_interceptor", "Lcom/navercorp/pinpoint/bootstrap/interceptor/Interceptor;");
        push(instructions, interceptorId);
        instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(InterceptorRegistry.class), "getInterceptor", "(I)" + Type.getDescriptor(Interceptor.class), false));
        storeVar(instructions, this.interceptorVarIndex);
        this.resultVarIndex = addInterceptorLocalVariable("_$PINPOINT$_result", "Ljava/lang/Object;");
        loadNull(instructions);
        storeVar(instructions, this.resultVarIndex);
        this.throwableVarIndex = addInterceptorLocalVariable("_$PINPOINT$_throwable", "Ljava/lang/Throwable;");
        loadNull(instructions);
        storeVar(instructions, this.throwableVarIndex);
    }

    private void initArgsVar(final InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.argsVarIndex = addInterceptorLocalVariable("_$PINPOINT$_args", "[Ljava/lang/Object;");
        loadArgsVar(instructions);
        storeVar(instructions, this.argsVarIndex);
    }

    private void initClassNameVar(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.classNameVarIndex = addInterceptorLocalVariable("_$PINPOINT$_className", "Ljava/lang/String;");
        push(instructions, JavaAssistUtils.jvmNameToJavaName(this.declaringClassInternalName));
        storeVar(instructions, this.classNameVarIndex);
    }

    private void initMethodNameVar(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.methodNameVarIndex = addInterceptorLocalVariable("_$PINPOINT$_methodName", "Ljava/lang/String;");
        push(instructions, this.methodNode.name);
        storeVar(instructions, this.methodNameVarIndex);
    }

    private void initParameterDescriptionVar(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.parameterDescriptionVarIndex = addInterceptorLocalVariable("_$PINPOINT$_parameterDescription", "Ljava/lang/String;");
        push(instructions, this.methodNode.desc);
        storeVar(instructions, this.parameterDescriptionVarIndex);
    }

    private void initApiIdVar(int apiId, InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.apiIdVarIndex = addInterceptorLocalVariable("_$PINPOINT$_apiId", "I");
        push(instructions, apiId);
        storeInt(instructions, this.apiIdVarIndex);
    }

    private void initArg0Var(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.arg0VarIndex = addInterceptorLocalVariable("_$PINPOINT$_arg0", "Ljava/lang/String;");
        loadArg(instructions, this.argumentTypes, 0);
        box(instructions, this.argumentTypes[0]);
        storeVar(instructions, this.arg0VarIndex);
    }

    private void initArg1Var(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.arg1VarIndex = addInterceptorLocalVariable("_$PINPOINT$_arg1", "Ljava/lang/String;");
        loadArg(instructions, this.argumentTypes, 1);
        box(instructions, this.argumentTypes[1]);
        storeVar(instructions, this.arg1VarIndex);
    }

    private void initArg2Var(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.arg2VarIndex = addInterceptorLocalVariable("_$PINPOINT$_arg2", "Ljava/lang/String;");
        loadArg(instructions, this.argumentTypes, 2);
        box(instructions, this.argumentTypes[2]);
        storeVar(instructions, this.arg2VarIndex);
    }

    private void initArg3Var(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.arg3VarIndex = addInterceptorLocalVariable("_$PINPOINT$_arg3", "Ljava/lang/String;");
        loadArg(instructions, this.argumentTypes, 3);
        box(instructions, this.argumentTypes[3]);
        storeVar(instructions, this.arg3VarIndex);
    }

    private void initArg4Var(InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        this.arg4VarIndex = addInterceptorLocalVariable("_$PINPOINT$_arg4", "Ljava/lang/String;");
        loadArg(instructions, this.argumentTypes, 4);
        box(instructions, this.argumentTypes[4]);
        storeVar(instructions, this.arg4VarIndex);
    }

    int getInterceptorParameterCount(final InterceptorDefinition interceptorDefinition) {
        if (interceptorDefinition.getBeforeMethod() != null) {
            // skip this.
            return interceptorDefinition.getBeforeMethod().getParameterTypes().length - 1;
        } else if (interceptorDefinition.getAfterMethod() != null) {
            // skip this, result, throwable.
            return interceptorDefinition.getAfterMethod().getParameterTypes().length - 3;
        }

        return 0;
    }

    public void storeThrowableVar(final InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        storeVar(instructions, this.throwableVarIndex);
        loadNull(instructions);
        storeVar(instructions, this.resultVarIndex);
    }

    public void storeResultVar(final InsnList instructions, final int opcode) {
        assertInitializedInterceptorLocalVariables();
        if (opcode == Opcodes.RETURN) {
            // void.
            loadNull(instructions);
        } else if (opcode == Opcodes.ARETURN) {
            // object.
            dup(instructions);
        } else {
            if (opcode == Opcodes.LRETURN || opcode == Opcodes.DRETURN) {
                // long or double.
                dup2(instructions);
            } else {
                dup(instructions);
            }
            final Type type = Type.getReturnType(this.methodNode.desc);
            box(instructions, type);
        }
        storeVar(instructions, this.resultVarIndex);
        loadNull(instructions);
        storeVar(instructions, this.throwableVarIndex);
    }

    public void loadInterceptorLocalVariables(final InsnList instructions, final InterceptorDefinition interceptorDefinition, final boolean after) {
        assertInitializedInterceptorLocalVariables();
        loadVar(instructions, this.interceptorVarIndex);
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(interceptorDefinition.getInterceptorBaseClass())));

        // target(this) object.
        loadThis(instructions);

        final InterceptorType interceptorType = interceptorDefinition.getInterceptorType();
        if (interceptorType == InterceptorType.ARRAY_ARGS) {
            // Object target, Object[] args
            loadVar(instructions, this.argsVarIndex);
        } else if (interceptorType == InterceptorType.STATIC) {
            // Object target, String declaringClassInternalName, String methodName, String parameterDescription, Object[] args
            loadVar(instructions, this.classNameVarIndex);
            loadVar(instructions, this.methodNameVarIndex);
            loadVar(instructions, this.parameterDescriptionVarIndex);
            loadVar(instructions, this.argsVarIndex);
        } else if (interceptorType == InterceptorType.API_ID_AWARE) {
            // Object target, int apiId, Object[] args
            loadInt(instructions, this.apiIdVarIndex);
            loadVar(instructions, this.argsVarIndex);
        } else if (interceptorType == InterceptorType.BASIC) {
            int interceptorMethodParameterCount = getInterceptorParameterCount(interceptorDefinition);
            int argumentCount = Math.min(this.argumentTypes.length, interceptorMethodParameterCount);
            int i = 1;
            for (; i <= argumentCount; i++) {
                // Object target, Object arg0, Object arg1, Object arg2, Object arg3, Object arg4
                if (i == 1) loadVar(instructions, this.arg0VarIndex);
                else if (i == 2) loadVar(instructions, this.arg1VarIndex);
                else if (i == 3) loadVar(instructions, this.arg2VarIndex);
                else if (i == 4) loadVar(instructions, this.arg3VarIndex);
                else if (i == 5) loadVar(instructions, this.arg4VarIndex);
                else loadNull(instructions);
            }

            for (; i <= interceptorMethodParameterCount; i++) {
                loadNull(instructions);
            }
        }

        if (after) {
            loadVar(instructions, this.resultVarIndex);
            loadVar(instructions, this.throwableVarIndex);
        }
    }

    public void loadInterceptorThrowVar(final InsnList instructions) {
        assertInitializedInterceptorLocalVariables();
        loadVar(instructions, this.throwableVarIndex);
        instructions.add(new InsnNode(Opcodes.ATHROW));
    }

    void loadThis(final InsnList instructions) {
        if (isConstructor()) {
            // load this.
            loadVar(instructions, 0);
        } else {
            if (isStatic()) {
                // load null.
                loadNull(instructions);
            } else {
                // load this.
                loadVar(instructions, 0);
            }
        }
    }

    void storeVar(final InsnList instructions, final int index) {
        instructions.add(new VarInsnNode(Opcodes.ASTORE, index));
    }

    void storeInt(final InsnList instructions, final int index) {
        instructions.add(new VarInsnNode(Opcodes.ISTORE, index));
    }

    void loadNull(final InsnList instructions) {
        instructions.add(new InsnNode(Opcodes.ACONST_NULL));
    }

    void loadVar(final InsnList instructions, final int index) {
        instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
    }

    void loadInt(final InsnList instructions, final int index) {
        instructions.add(new VarInsnNode(Opcodes.ILOAD, index));
    }

    boolean isReturnCode(final int opcode) {
        return opcode == Opcodes.IRETURN || opcode == Opcodes.LRETURN || opcode == Opcodes.FRETURN || opcode == Opcodes.DRETURN || opcode == Opcodes.ARETURN || opcode == Opcodes.RETURN;
    }

    Type getBoxedType(final Type type) {
        switch (type.getSort()) {
            case Type.BYTE:
                return BYTE_TYPE;
            case Type.BOOLEAN:
                return BOOLEAN_TYPE;
            case Type.SHORT:
                return SHORT_TYPE;
            case Type.CHAR:
                return CHARACTER_TYPE;
            case Type.INT:
                return INTEGER_TYPE;
            case Type.FLOAT:
                return FLOAT_TYPE;
            case Type.LONG:
                return LONG_TYPE;
            case Type.DOUBLE:
                return DOUBLE_TYPE;
        }
        return type;
    }

    void push(InsnList insnList, final int value) {
        if (value >= -1 && value <= 5) {
            insnList.add(new InsnNode(Opcodes.ICONST_0 + value));
        } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
            insnList.add(new IntInsnNode(Opcodes.BIPUSH, value));
        } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
            insnList.add(new IntInsnNode(Opcodes.SIPUSH, value));
        } else {
            insnList.add(new LdcInsnNode(value));
        }
    }

    void push(InsnList insnList, final String value) {
        if (value == null) {
            insnList.add(new InsnNode(Opcodes.ACONST_NULL));
        } else {
            insnList.add(new LdcInsnNode(value));
        }
    }


    void newArray(final InsnList insnList, final Type type) {
        insnList.add(new TypeInsnNode(Opcodes.ANEWARRAY, type.getInternalName()));
    }


    void dup(final InsnList insnList) {
        insnList.add(new InsnNode(Opcodes.DUP));
    }

    void dup2(final InsnList insnList) {
        insnList.add(new InsnNode(Opcodes.DUP2));
    }

    void dupX1(final InsnList insnList) {
        insnList.add(new InsnNode(Opcodes.DUP_X1));
    }

    void dupX2(final InsnList insnList) {
        insnList.add(new InsnNode(Opcodes.DUP_X2));
    }

    void pop(final InsnList insnList) {
        insnList.add(new InsnNode(Opcodes.POP));
    }

    void swap(final InsnList insnList) {
        insnList.add(new InsnNode(Opcodes.SWAP));
    }

    void loadArgsVar(final InsnList instructions) {
        if (this.argumentTypes.length == 0) {
            // null.
            loadNull(instructions);
            return;
        }

        push(instructions, this.argumentTypes.length);
        // new array
        newArray(instructions, OBJECT_TYPE);
        for (int i = 0; i < this.argumentTypes.length; i++) {
            Type type = this.argumentTypes[i];
            dup(instructions);
            push(instructions, i);
            // loadArg
            loadArg(instructions, this.argumentTypes, i);
            // box
            box(instructions, type);
            // arrayStore
            arrayStore(instructions, OBJECT_TYPE);
        }
    }

    void loadArgs(final InsnList instructions) {
        for (int i = 0; i < this.argumentTypes.length; i++) {
            loadArg(instructions, this.argumentTypes, i);
        }
    }

    void loadArg(final InsnList instructions, Type[] argumentTypes, int i) {
        final int index = getArgIndex(argumentTypes, i);
        final Type type = argumentTypes[i];
        instructions.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), index));
    }

    int getArgIndex(final Type[] argumentTypes, final int arg) {
        int index = isStatic() ? 0 : 1;
        for (int i = 0; i < arg; i++) {
            index += argumentTypes[i].getSize();
        }
        return index;
    }

    void box(final InsnList instructions, Type type) {
        if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
            return;
        }

        if (type == Type.VOID_TYPE) {
            // push null
            instructions.add(new InsnNode(Opcodes.ACONST_NULL));
        } else {
            Type boxed = getBoxedType(type);
            // new instance.
            newInstance(instructions, boxed);
            if (type.getSize() == 2) {
                // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
                // dupX2
                dupX2(instructions);
                // dupX2
                dupX2(instructions);
                // pop
                pop(instructions);
            } else {
                // p -> po -> opo -> oop -> o
                // dupX1
                dupX1(instructions);
                // swap
                swap(instructions);
            }
            invokeConstructor(instructions, boxed, new Method("<init>", Type.VOID_TYPE, new Type[]{type}));
        }
    }

    void arrayStore(final InsnList instructions, final Type type) {
        instructions.add(new InsnNode(type.getOpcode(Opcodes.IASTORE)));
    }

    void newInstance(final InsnList instructions, final Type type) {
        instructions.add(new TypeInsnNode(Opcodes.NEW, type.getInternalName()));
    }

    void invokeConstructor(final InsnList instructions, final Type type, final Method method) {
        String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName();
        instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, owner, method.getName(), method.getDescriptor(), false));
    }


    int addInterceptorLocalVariable(final String name, final String desc) {
        return addLocalVariable(name, desc, this.interceptorVariableStartLabelNode, this.interceptorVariableEndLabelNode);
    }

    int addLocalVariable(final String name, final String desc, final LabelNode start, final LabelNode end) {
        int index = this.nextLocals;
        this.nextLocals += 1;
        final LocalVariableNode node = new LocalVariableNode(name, desc, null, start, end, index);
        this.methodNode.localVariables.add(node);

        return index;
    }

    public void returnValue(final InsnList instructions) {
        instructions.add(new InsnNode(this.returnType.getOpcode(Opcodes.IRETURN)));
    }

    private boolean isStatic() {
        return (this.methodNode.access & Opcodes.ACC_STATIC) != 0;
    }

    private boolean isConstructor() {
        return this.methodNode.name != null && this.methodNode.name.equals("<init>");
    }

    private void assertInitializedInterceptorLocalVariables() {
        if (!this.initializedInterceptorLocalVariables) {
            throw new IllegalStateException("The interceptor local variables must be initialized.");
        }
    }

    // test only.
    List<LocalVariableNode> getLocalVariables() {
        return this.methodNode.localVariables;
    }
}