/***
 * ASM: a very small and fast Java bytecode manipulation framework
 * Copyright (c) 2000-2011 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 org.apache.tapestry5.internal.plastic.asm.commons;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.tapestry5.internal.plastic.asm.Handle;
import org.apache.tapestry5.internal.plastic.asm.Label;
import org.apache.tapestry5.internal.plastic.asm.MethodVisitor;
import org.apache.tapestry5.internal.plastic.asm.Opcodes;
import org.apache.tapestry5.internal.plastic.asm.Type;

/**
 * A {@link org.apache.tapestry5.internal.plastic.asm.MethodVisitor} to insert before, after and around
 * advices in methods and constructors.
 * <p>
 * The behavior for constructors is like this:
 * <ol>
 * 
 * <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>
 * 
 * <li>when this one is reached, it is only added in the ctor code visitor and a
 * JP invoke is added</li>
 * 
 * <li>after that, only the other code visitor receives the instructions</li>
 * 
 * </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 List<Object> stackFrame;

    private Map<Label, List<Object>> branches;

    /**
     * Creates a new {@link AdviceAdapter}.
     * 
     * @param api
     *            the ASM API version implemented by this visitor. Must be one
     *            of {@link Opcodes#ASM4} or {@link Opcodes#ASM5}.
     * @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}).
     */
    protected AdviceAdapter(final int api, final MethodVisitor mv,
            final int access, final String name, final String desc) {
        super(api, mv, access, name, desc);
        methodAccess = access;
        methodDesc = desc;
        constructor = "<init>".equals(name);
    }

    @Override
    public void visitCode() {
        mv.visitCode();
        if (constructor) {
            stackFrame = new ArrayList<Object>();
            branches = new HashMap<Label, List<Object>>();
        } else {
            superInitialized = true;
            onMethodEnter();
        }
    }

    @Override
    public void visitLabel(final Label label) {
        mv.visitLabel(label);
        if (constructor && branches != null) {
            List<Object> frame = branches.get(label);
            if (frame != null) {
                stackFrame = frame;
                branches.remove(label);
            }
        }
    }

    @Override
    public void visitInsn(final int opcode) {
        if (constructor) {
            int s;
            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();
                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 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:
                s = stackFrame.size();
                stackFrame.add(s - 2, stackFrame.get(s - 1));
                break;
            case DUP_X2:
                s = stackFrame.size();
                stackFrame.add(s - 3, stackFrame.get(s - 1));
                break;
            case DUP2:
                s = stackFrame.size();
                stackFrame.add(s - 2, stackFrame.get(s - 1));
                stackFrame.add(s - 2, stackFrame.get(s - 1));
                break;
            case DUP2_X1:
                s = stackFrame.size();
                stackFrame.add(s - 3, stackFrame.get(s - 1));
                stackFrame.add(s - 3, stackFrame.get(s - 1));
                break;
            case DUP2_X2:
                s = stackFrame.size();
                stackFrame.add(s - 4, stackFrame.get(s - 1));
                stackFrame.add(s - 4, stackFrame.get(s - 1));
                break;
            case SWAP:
                s = stackFrame.size();
                stackFrame.add(s - 2, stackFrame.get(s - 1));
                stackFrame.remove(s);
                break;
            }
        } else {
            switch (opcode) {
            case RETURN:
            case IRETURN:
            case FRETURN:
            case ARETURN:
            case LRETURN:
            case DRETURN:
            case ATHROW:
                onMethodExit(opcode);
                break;
            }
        }
        mv.visitInsn(opcode);
    }

    @Override
    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;
            }
        }
    }

    @Override
    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);
                }
            }
        }
    }

    @Override
    public void visitIntInsn(final int opcode, final int operand) {
        mv.visitIntInsn(opcode, operand);
        if (constructor && opcode != NEWARRAY) {
            pushValue(OTHER);
        }
    }

    @Override
    public void visitLdcInsn(final Object cst) {
        mv.visitLdcInsn(cst);
        if (constructor) {
            pushValue(OTHER);
            if (cst instanceof Double || cst instanceof Long) {
                pushValue(OTHER);
            }
        }
    }

    @Override
    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);
        }
    }

    @Override
    public void visitTypeInsn(final int opcode, final String type) {
        mv.visitTypeInsn(opcode, type);
        // ANEWARRAY, CHECKCAST or INSTANCEOF don't change stack
        if (constructor && opcode == NEW) {
            pushValue(OTHER);
        }
    }

    @Deprecated
    @Override
    public void visitMethodInsn(final int opcode, final String owner,
            final String name, final String desc) {
        if (api >= Opcodes.ASM5) {
            super.visitMethodInsn(opcode, owner, name, desc);
            return;
        }
        doVisitMethodInsn(opcode, owner, name, desc,
                opcode == Opcodes.INVOKEINTERFACE);
    }

    @Override
    public void visitMethodInsn(final int opcode, final String owner,
            final String name, final String desc, final boolean itf) {
        if (api < Opcodes.ASM5) {
            super.visitMethodInsn(opcode, owner, name, desc, itf);
            return;
        }
        doVisitMethodInsn(opcode, owner, name, desc, itf);
    }

    private void doVisitMethodInsn(int opcode, final String owner,
            final String name, final String desc, final boolean itf) {
        mv.visitMethodInsn(opcode, owner, name, desc, itf);
        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);
                }
            }
        }
    }

    @Override
    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm,
            Object... bsmArgs) {
        mv.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
        if (constructor) {
            Type[] types = Type.getArgumentTypes(desc);
            for (int i = 0; i < types.length; i++) {
                popValue();
                if (types[i].getSize() == 2) {
                    popValue();
                }
            }

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

    @Override
    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);
        }
    }

    @Override
    public void visitLookupSwitchInsn(final Label dflt, final int[] keys,
            final Label[] labels) {
        mv.visitLookupSwitchInsn(dflt, keys, labels);
        if (constructor) {
            popValue();
            addBranches(dflt, labels);
        }
    }

    @Override
    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);
        }
    }

    @Override
    public void visitTryCatchBlock(Label start, Label end, Label handler,
            String type) {
        super.visitTryCatchBlock(start, end, handler, type);
        if (constructor && !branches.containsKey(handler)) {
            List<Object> stackFrame = new ArrayList<Object>();
            stackFrame.add(OTHER);
            branches.put(handler, stackFrame);
        }
    }

    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;
        }
        branches.put(label, new ArrayList<Object>(stackFrame));
    }

    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>
     * 
     * <i>Custom code can use or change all the local variables, but should not
     * change state of the stack.</i>
     */
    protected 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:
     * 
     * <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");
     *   }
     * 
     *   // an actual call back method
     *   public static void onExit(Object param, int opcode) {
     *     ...
     * </pre>
     * 
     * <br>
     * <br>
     * 
     * <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 void onMethodExit(int opcode) {
    }

    // TODO onException, onMethodCall
}
