/**************************************************************************************
 * Copyright (c) Jonas BonŽr, Alexandre Vasseur. All rights reserved.                 *
 * http://aspectwerkz.codehaus.org                                                    *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the LGPL license      *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.transform.inlining.weaver;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.CodeVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.CodeAdapter;
import org.objectweb.asm.Label;
import org.codehaus.aspectwerkz.definition.SystemDefinition;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.expression.PointcutType;
import org.codehaus.aspectwerkz.joinpoint.management.JoinPointType;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.reflect.MemberInfo;
import org.codehaus.aspectwerkz.reflect.ClassInfoHelper;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.transform.Context;
import org.codehaus.aspectwerkz.transform.TransformationUtil;
import org.codehaus.aspectwerkz.transform.TransformationConstants;
import org.codehaus.aspectwerkz.transform.inlining.compiler.AbstractJoinPointCompiler;
import org.codehaus.aspectwerkz.transform.inlining.ContextImpl;
import org.codehaus.aspectwerkz.transform.inlining.AsmHelper;
import org.codehaus.aspectwerkz.transform.inlining.EmittedJoinPoint;

import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.Set;

/**
 * Instruments method CALL join points by replacing INVOKEXXX instructions with invocations of the compiled join point.
 * <br/>
 * It calls the JPClass.invoke static method. The signature of the invoke method depends if the
 * target method is static or not as follow:
 * <pre>
 *      invoke(callee, args.., caller) // non static
 *      invoke(args.., caller) // static
 * </pre>
 * (The reason why is that it simplifies call pointcut stack management)
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a>
 */
public class MethodCallVisitor extends ClassAdapter implements TransformationConstants {

    private final ContextImpl m_ctx;
    private final ClassLoader m_loader;
    private final ClassInfo m_callerClassInfo;

    private Label m_lastLabelForLineNumber = EmittedJoinPoint.NO_LINE_NUMBER;

    /**
     * Creates a new instance.
     *
     * @param cv
     * @param loader
     * @param classInfo
     * @param ctx
     */
    public MethodCallVisitor(final ClassVisitor cv,
                             final ClassLoader loader,
                             final ClassInfo classInfo,
                             final Context ctx) {
        super(cv);
        m_loader = loader;
        m_callerClassInfo = classInfo;
        m_ctx = (ContextImpl) ctx;
    }

    /**
     * Visits the caller methods.
     *
     * @param access
     * @param name
     * @param desc
     * @param exceptions
     * @param attrs
     * @return
     */
    public CodeVisitor visitMethod(final int access,
                                   final String name,
                                   final String desc,
                                   final String[] exceptions,
                                   final Attribute attrs) {

        if (name.startsWith(WRAPPER_METHOD_PREFIX) ||
            Modifier.isNative(access) ||
            Modifier.isAbstract(access)) {
            return super.visitMethod(access, name, desc, exceptions, attrs);
        }

        CodeVisitor mv = cv.visitMethod(access, name, desc, exceptions, attrs);
        return mv == null ? null : new ReplaceInvokeInstructionCodeAdapter(
                mv,
                m_loader,
                m_callerClassInfo,
                m_ctx.getClassName(),
                name,
                desc
        );
    }

    /**
     * Replaces 'INVOKEXXX' instructions with a call to the compiled JoinPoint instance.
     *
     * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér </a>
     * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a>
     */
    public class ReplaceInvokeInstructionCodeAdapter extends AfterObjectInitializationCodeAdapter {

        private final ClassLoader m_loader;
        private final ClassInfo m_callerClassInfo;
        private final String m_callerClassName;
        private final String m_callerMethodName;
        private final String m_callerMethodDesc;
        private final MemberInfo m_callerMemberInfo;

        /**
         * Creates a new instance.
         *
         * @param ca
         * @param loader
         * @param callerClassInfo
         * @param callerClassName
         * @param callerMethodName
         * @param callerMethodDesc
         */
        public ReplaceInvokeInstructionCodeAdapter(final CodeVisitor ca,
                                                   final ClassLoader loader,
                                                   final ClassInfo callerClassInfo,
                                                   final String callerClassName,
                                                   final String callerMethodName,
                                                   final String callerMethodDesc) {
            super(ca, callerMethodName);
            m_loader = loader;
            m_callerClassInfo = callerClassInfo;
            m_callerClassName = callerClassName;
            m_callerMethodName = callerMethodName;
            m_callerMethodDesc = callerMethodDesc;

            if (CLINIT_METHOD_NAME.equals(callerMethodName)) {
                m_callerMemberInfo = m_callerClassInfo.staticInitializer();
            } else if (INIT_METHOD_NAME.equals(callerMethodName)) {
                int hash = AsmHelper.calculateConstructorHash(m_callerMethodDesc);
                m_callerMemberInfo = m_callerClassInfo.getConstructor(hash);
            } else {
                int hash = AsmHelper.calculateMethodHash(m_callerMethodName, m_callerMethodDesc);
                m_callerMemberInfo = m_callerClassInfo.getMethod(hash);
            }
        }

        /**
         * Label
         *
         * @param label
         */
        public void visitLabel(Label label) {
            m_lastLabelForLineNumber = label;
            super.visitLabel(label);
        }

        /**
         * Visits 'INVOKEXXX' instructions.
         *
         * @param opcode
         * @param calleeClassName
         * @param calleeMethodName
         * @param calleeMethodDesc
         */
        public void visitMethodInsn(final int opcode,
                                    String calleeClassName,
                                    final String calleeMethodName,
                                    final String calleeMethodDesc) {

            if (m_callerMemberInfo == null) {
                System.err.println(
                        "AW::WARNING " +
                        "metadata structure could not be build for method ["
                        + m_callerClassInfo.getName().replace('/', '.')
                        + '.' + m_callerMethodName + ':' + m_callerMethodDesc + ']'
                );
                super.visitMethodInsn(opcode, calleeClassName, calleeMethodName, calleeMethodDesc);
                return;
            }

            if (INIT_METHOD_NAME.equals(calleeMethodName) ||
                CLINIT_METHOD_NAME.equals(calleeMethodName) ||
                calleeMethodName.startsWith(ASPECTWERKZ_PREFIX)
                || calleeClassName.endsWith(JOIN_POINT_CLASS_SUFFIX)
                //calleeClassName.startsWith("org/aopalliance/")) { // FIXME make generic fix by invoking all AspectModels (same problem in other visitors as well)
                    ){super.visitMethodInsn(opcode, calleeClassName, calleeMethodName, calleeMethodDesc);
                return;
            }

            // check if we have a super.sameMethod() call
            if (opcode == INVOKESPECIAL
                && !calleeClassName.equals(m_callerClassName)
                && ClassInfoHelper.extendsSuperClass(m_callerClassInfo, calleeClassName.replace('/', '.'))) {
                super.visitMethodInsn(opcode, calleeClassName, calleeMethodName, calleeMethodDesc);
                return;
            }

            // check if object initialization has been reached
            if (!m_isObjectInitialized) {
                super.visitMethodInsn(opcode, calleeClassName, calleeMethodName, calleeMethodDesc);
                return;
            }

            int joinPointHash = AsmHelper.calculateMethodHash(calleeMethodName, calleeMethodDesc);

            ClassInfo classInfo = AsmClassInfo.getClassInfo(calleeClassName, m_loader);
            MethodInfo calleeMethodInfo = classInfo.getMethod(joinPointHash);

            if (calleeMethodInfo == null) {
                System.err.println(
                        "AW::WARNING " +
                        "metadata structure could not be build for method ["
                        + classInfo.getName().replace('/', '.')
                        + '.' + calleeMethodName + ':' + calleeMethodDesc
                        + "] when parsing method ["
                        + m_callerClassInfo.getName() + '.' + m_callerMethodName + "(..)]"
                );
                // bail out
                super.visitMethodInsn(opcode, calleeClassName, calleeMethodName, calleeMethodDesc);
                return;
            }

            ExpressionContext ctx = new ExpressionContext(PointcutType.CALL, calleeMethodInfo, m_callerMemberInfo);

            if (methodFilter(m_ctx.getDefinitions(), ctx, calleeMethodInfo)) {
                super.visitMethodInsn(opcode, calleeClassName, calleeMethodName, calleeMethodDesc);
            } else {
                m_ctx.markAsAdvised();

                String joinPointClassName = TransformationUtil.getJoinPointClassName(
                        m_callerClassName,
                        m_callerMethodName,
                        m_callerMethodDesc,
                        calleeClassName,
                        JoinPointType.METHOD_CALL_INT,
                        joinPointHash
                );

                // load the caller instance (this), or null if in a static context
                // note that callee instance [optional] and args are already on the stack
                if (Modifier.isStatic(m_callerMemberInfo.getModifiers())) {
                    visitInsn(ACONST_NULL);
                } else {
                    visitVarInsn(ALOAD, 0);
                }

                // add the call to the join point
                super.visitMethodInsn(
                        INVOKESTATIC,
                        joinPointClassName,
                        INVOKE_METHOD_NAME,
                        TransformationUtil.getInvokeSignatureForCodeJoinPoints(
                                calleeMethodInfo.getModifiers(), calleeMethodDesc,
                                m_callerClassName, calleeClassName
                        )
                );

                // emit the joinpoint
                //See AW-253 - we remember if we had an INVOKE INTERFACE opcode
                int modifiers = calleeMethodInfo.getModifiers();
                if (opcode == INVOKEINTERFACE) {
                    modifiers = modifiers | MODIFIER_INVOKEINTERFACE;
                }
                m_ctx.addEmittedJoinPoint(
                        new EmittedJoinPoint(
                                JoinPointType.METHOD_CALL_INT,
                                m_callerClassName,
                                m_callerMethodName,
                                m_callerMethodDesc,
                                m_callerMemberInfo.getModifiers(),
                                calleeClassName,
                                calleeMethodName,
                                calleeMethodDesc,
                                modifiers,
                                joinPointHash,
                                joinPointClassName,
                                m_lastLabelForLineNumber
                        )
                );
            }
        }

        /**
         * Filters out the methods that are not eligible for transformation.
         * Do not filter on abstract callee method - needed for interface declared method call
         * (invokeinterface instr.)
         *
         * @param definitions
         * @param ctx
         * @param calleeMethodInfo
         * @return boolean true if the method should be filtered out
         */
        public boolean methodFilter(final Set definitions,
                                    final ExpressionContext ctx,
                                    final MethodInfo calleeMethodInfo) {
            if (calleeMethodInfo.getName().equals(INIT_METHOD_NAME) ||
                calleeMethodInfo.getName().equals(CLINIT_METHOD_NAME) ||
                calleeMethodInfo.getName().startsWith(ORIGINAL_METHOD_PREFIX)) {
                return true;
            }
            for (Iterator it = definitions.iterator(); it.hasNext();) {
                if (((SystemDefinition) it.next()).hasPointcut(ctx)) {
                    return false;
                } else {
                    continue;
                }
            }
            return true;
        }
    }
}