/***
 * ASM: a very small and fast Java bytecode manipulation framework
 * Copyright (c) 2000-2005 INRIA, France Telecom
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package clojure.asm.commons;

import java.util.ArrayList;
import java.util.HashMap;

import clojure.asm.Label;
import clojure.asm.MethodVisitor;
import clojure.asm.Opcodes;
import clojure.asm.Type;

/**
 * A {@link clojure.asm.MethodAdapter} to insert before, after and around
 * advices in methods and constructors. <p> The behavior for constructors is
 * like this: <ol>
 * <p/>
 * <li>as long as the INVOKESPECIAL for the object initialization has not been
 * reached, every bytecode instruction is dispatched in the ctor code visitor</li>
 * <p/>
 * <li>when this one is reached, it is only added in the ctor code visitor and
 * a JP invoke is added</li>
 * <p/>
 * <li>after that, only the other code visitor receives the instructions</li>
 * <p/>
 * </ol>
 *
 * @author Eugene Kuleshov
 * @author Eric Bruneton
 */
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes{
private static final Object THIS = new Object();
private static final Object OTHER = new Object();

protected int methodAccess;
protected String methodDesc;

private boolean constructor;
private boolean superInitialized;
private ArrayList stackFrame;
private HashMap branches;

/**
 * Creates a new {@link AdviceAdapter}.
 *
 * @param mv     the method visitor to which this adapter delegates calls.
 * @param access the method's access flags (see {@link Opcodes}).
 * @param name   the method's name.
 * @param desc   the method's descriptor (see {@link Type Type}).
 */
public AdviceAdapter(
		final MethodVisitor mv,
		final int access,
		final String name,
		final String desc){
	super(mv, access, name, desc);
	methodAccess = access;
	methodDesc = desc;

	constructor = "<init>".equals(name);
}

public void visitCode(){
	mv.visitCode();
	if(!constructor)
		{
		superInitialized = true;
		onMethodEnter();
		}
	else
		{
		stackFrame = new ArrayList();
		branches = new HashMap();
		}
}

public void visitLabel(final Label label){
	mv.visitLabel(label);

	if(constructor && branches != null)
		{
		ArrayList frame = (ArrayList) branches.get(label);
		if(frame != null)
			{
			stackFrame = frame;
			branches.remove(label);
			}
		}
}

public void visitInsn(final int opcode){
	if(constructor)
		{
		switch(opcode)
			{
			case RETURN: // empty stack
				onMethodExit(opcode);
				break;

			case IRETURN: // 1 before n/a after
			case FRETURN: // 1 before n/a after
			case ARETURN: // 1 before n/a after
			case ATHROW: // 1 before n/a after
				popValue();
				popValue();
				onMethodExit(opcode);
				break;

			case LRETURN: // 2 before n/a after
			case DRETURN: // 2 before n/a after
				popValue();
				popValue();
				onMethodExit(opcode);
				break;

			case NOP:
			case LALOAD: // remove 2 add 2
			case DALOAD: // remove 2 add 2
			case LNEG:
			case DNEG:
			case FNEG:
			case INEG:
			case L2D:
			case D2L:
			case F2I:
			case I2B:
			case I2C:
			case I2S:
			case I2F:
			case Opcodes.ARRAYLENGTH:
				break;

			case ACONST_NULL:
			case ICONST_M1:
			case ICONST_0:
			case ICONST_1:
			case ICONST_2:
			case ICONST_3:
			case ICONST_4:
			case ICONST_5:
			case FCONST_0:
			case FCONST_1:
			case FCONST_2:
			case F2L: // 1 before 2 after
			case F2D:
			case I2L:
			case I2D:
				pushValue(OTHER);
				break;

			case LCONST_0:
			case LCONST_1:
			case DCONST_0:
			case DCONST_1:
				pushValue(OTHER);
				pushValue(OTHER);
				break;

			case IALOAD: // remove 2 add 1
			case FALOAD: // remove 2 add 1
			case AALOAD: // remove 2 add 1
			case BALOAD: // remove 2 add 1
			case CALOAD: // remove 2 add 1
			case SALOAD: // remove 2 add 1
			case POP:
			case IADD:
			case FADD:
			case ISUB:
			case LSHL: // 3 before 2 after
			case LSHR: // 3 before 2 after
			case LUSHR: // 3 before 2 after
			case L2I: // 2 before 1 after
			case L2F: // 2 before 1 after
			case D2I: // 2 before 1 after
			case D2F: // 2 before 1 after
			case FSUB:
			case FMUL:
			case FDIV:
			case FREM:
			case FCMPL: // 2 before 1 after
			case FCMPG: // 2 before 1 after
			case IMUL:
			case IDIV:
			case IREM:
			case ISHL:
			case ISHR:
			case IUSHR:
			case IAND:
			case IOR:
			case IXOR:
			case MONITORENTER:
			case MONITOREXIT:
				popValue();
				break;

			case POP2:
			case LSUB:
			case LMUL:
			case LDIV:
			case LREM:
			case LADD:
			case LAND:
			case LOR:
			case LXOR:
			case DADD:
			case DMUL:
			case DSUB:
			case DDIV:
			case DREM:
				popValue();
				popValue();
				break;

			case IASTORE:
			case FASTORE:
			case AASTORE:
			case BASTORE:
			case CASTORE:
			case SASTORE:
			case LCMP: // 4 before 1 after
			case DCMPL:
			case DCMPG:
				popValue();
				popValue();
				popValue();
				break;

			case LASTORE:
			case DASTORE:
				popValue();
				popValue();
				popValue();
				popValue();
				break;

			case DUP:
				pushValue(peekValue());
				break;

			case DUP_X1:
				// TODO optimize this
			{
			Object o1 = popValue();
			Object o2 = popValue();
			pushValue(o1);
			pushValue(o2);
			pushValue(o1);
			}
			break;

			case DUP_X2:
				// TODO optimize this
			{
			Object o1 = popValue();
			Object o2 = popValue();
			Object o3 = popValue();
			pushValue(o1);
			pushValue(o3);
			pushValue(o2);
			pushValue(o1);
			}
			break;

			case DUP2:
				// TODO optimize this
			{
			Object o1 = popValue();
			Object o2 = popValue();
			pushValue(o2);
			pushValue(o1);
			pushValue(o2);
			pushValue(o1);
			}
			break;

			case DUP2_X1:
				// TODO optimize this
			{
			Object o1 = popValue();
			Object o2 = popValue();
			Object o3 = popValue();
			pushValue(o2);
			pushValue(o1);
			pushValue(o3);
			pushValue(o2);
			pushValue(o1);
			}
			break;

			case DUP2_X2:
				// TODO optimize this
			{
			Object o1 = popValue();
			Object o2 = popValue();
			Object o3 = popValue();
			Object o4 = popValue();
			pushValue(o2);
			pushValue(o1);
			pushValue(o4);
			pushValue(o3);
			pushValue(o2);
			pushValue(o1);
			}
			break;

			case SWAP:
			{
			Object o1 = popValue();
			Object o2 = popValue();
			pushValue(o1);
			pushValue(o2);
			}
			break;
			}
		}
	else
		{
		switch(opcode)
			{
			case RETURN:
			case IRETURN:
			case FRETURN:
			case ARETURN:
			case LRETURN:
			case DRETURN:
			case ATHROW:
				onMethodExit(opcode);
				break;
			}
		}
	mv.visitInsn(opcode);
}

public void visitVarInsn(final int opcode, final int var){
	super.visitVarInsn(opcode, var);

	if(constructor)
		{
		switch(opcode)
			{
			case ILOAD:
			case FLOAD:
				pushValue(OTHER);
				break;
			case LLOAD:
			case DLOAD:
				pushValue(OTHER);
				pushValue(OTHER);
				break;
			case ALOAD:
				pushValue(var == 0 ? THIS : OTHER);
				break;
			case ASTORE:
			case ISTORE:
			case FSTORE:
				popValue();
				break;
			case LSTORE:
			case DSTORE:
				popValue();
				popValue();
				break;
			}
		}
}

public void visitFieldInsn(
		final int opcode,
		final String owner,
		final String name,
		final String desc){
	mv.visitFieldInsn(opcode, owner, name, desc);

	if(constructor)
		{
		char c = desc.charAt(0);
		boolean longOrDouble = c == 'J' || c == 'D';
		switch(opcode)
			{
			case GETSTATIC:
				pushValue(OTHER);
				if(longOrDouble)
					{
					pushValue(OTHER);
					}
				break;
			case PUTSTATIC:
				popValue();
				if(longOrDouble)
					{
					popValue();
					}
				break;
			case PUTFIELD:
				popValue();
				if(longOrDouble)
					{
					popValue();
					popValue();
					}
				break;
				// case GETFIELD:
			default:
				if(longOrDouble)
					{
					pushValue(OTHER);
					}
			}
		}
}

public void visitIntInsn(final int opcode, final int operand){
	mv.visitIntInsn(opcode, operand);

	if(constructor && opcode != NEWARRAY)
		{
		pushValue(OTHER);
		}
}

public void visitLdcInsn(final Object cst){
	mv.visitLdcInsn(cst);

	if(constructor)
		{
		pushValue(OTHER);
		if(cst instanceof Double || cst instanceof Long)
			{
			pushValue(OTHER);
			}
		}
}

public void visitMultiANewArrayInsn(final String desc, final int dims){
	mv.visitMultiANewArrayInsn(desc, dims);

	if(constructor)
		{
		for(int i = 0; i < dims; i++)
			{
			popValue();
			}
		pushValue(OTHER);
		}
}

public void visitTypeInsn(final int opcode, final String name){
	mv.visitTypeInsn(opcode, name);

	// ANEWARRAY, CHECKCAST or INSTANCEOF don't change stack
	if(constructor && opcode == NEW)
		{
		pushValue(OTHER);
		}
}

public void visitMethodInsn(
		final int opcode,
		final String owner,
		final String name,
		final String desc){
	mv.visitMethodInsn(opcode, owner, name, desc);

	if(constructor)
		{
		Type[] types = Type.getArgumentTypes(desc);
		for(int i = 0; i < types.length; i++)
			{
			popValue();
			if(types[i].getSize() == 2)
				{
				popValue();
				}
			}
		switch(opcode)
			{
			// case INVOKESTATIC:
			// break;

			case INVOKEINTERFACE:
			case INVOKEVIRTUAL:
				popValue(); // objectref
				break;

			case INVOKESPECIAL:
				Object type = popValue(); // objectref
				if(type == THIS && !superInitialized)
					{
					onMethodEnter();
					superInitialized = true;
					// once super has been initialized it is no longer
					// necessary to keep track of stack state
					constructor = false;
					}
				break;
			}

		Type returnType = Type.getReturnType(desc);
		if(returnType != Type.VOID_TYPE)
			{
			pushValue(OTHER);
			if(returnType.getSize() == 2)
				{
				pushValue(OTHER);
				}
			}
		}
}

public void visitJumpInsn(final int opcode, final Label label){
	mv.visitJumpInsn(opcode, label);

	if(constructor)
		{
		switch(opcode)
			{
			case IFEQ:
			case IFNE:
			case IFLT:
			case IFGE:
			case IFGT:
			case IFLE:
			case IFNULL:
			case IFNONNULL:
				popValue();
				break;

			case IF_ICMPEQ:
			case IF_ICMPNE:
			case IF_ICMPLT:
			case IF_ICMPGE:
			case IF_ICMPGT:
			case IF_ICMPLE:
			case IF_ACMPEQ:
			case IF_ACMPNE:
				popValue();
				popValue();
				break;

			case JSR:
				pushValue(OTHER);
				break;
			}
		addBranch(label);
		}
}

public void visitLookupSwitchInsn(
		final Label dflt,
		final int[] keys,
		final Label[] labels){
	mv.visitLookupSwitchInsn(dflt, keys, labels);

	if(constructor)
		{
		popValue();
		addBranches(dflt, labels);
		}
}

public void visitTableSwitchInsn(
		final int min,
		final int max,
		final Label dflt,
		final Label[] labels){
	mv.visitTableSwitchInsn(min, max, dflt, labels);

	if(constructor)
		{
		popValue();
		addBranches(dflt, labels);
		}
}

private void addBranches(final Label dflt, final Label[] labels){
	addBranch(dflt);
	for(int i = 0; i < labels.length; i++)
		{
		addBranch(labels[i]);
		}
}

private void addBranch(final Label label){
	if(branches.containsKey(label))
		{
		return;
		}
	ArrayList frame = new ArrayList();
	frame.addAll(stackFrame);
	branches.put(label, frame);
}

private Object popValue(){
	return stackFrame.remove(stackFrame.size() - 1);
}

private Object peekValue(){
	return stackFrame.get(stackFrame.size() - 1);
}

private void pushValue(final Object o){
	stackFrame.add(o);
}

/**
 * Called at the beginning of the method or after super class class call in
 * the constructor. <br><br>
 * <p/>
 * <i>Custom code can use or change all the local variables, but should not
 * change state of the stack.</i>
 */
protected abstract void onMethodEnter();

/**
 * Called before explicit exit from the method using either return or throw.
 * Top element on the stack contains the return value or exception instance.
 * For example:
 * <p/>
 * <pre>
 *   public void onMethodExit(int opcode) {
 *     if(opcode==RETURN) {
 *         visitInsn(ACONST_NULL);
 *     } else if(opcode==ARETURN || opcode==ATHROW) {
 *         dup();
 *     } else {
 *         if(opcode==LRETURN || opcode==DRETURN) {
 *             dup2();
 *         } else {
 *             dup();
 *         }
 *         box(Type.getReturnType(this.methodDesc));
 *     }
 *     visitIntInsn(SIPUSH, opcode);
 *     visitMethodInsn(INVOKESTATIC, owner, "onExit", "(Ljava/lang/Object;I)V");
 *   }
 * <p/>
 *   // an actual call back method
 *   public static void onExit(int opcode, Object param) {
 *     ...
 * </pre>
 * <p/>
 * <br><br>
 * <p/>
 * <i>Custom code can use or change all the local variables, but should not
 * change state of the stack.</i>
 *
 * @param opcode one of the RETURN, IRETURN, FRETURN, ARETURN, LRETURN,
 *               DRETURN or ATHROW
 */
protected abstract void onMethodExit(int opcode);

// TODO onException, onMethodCall

}
