/**************************************************************************************
 * 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;

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

import org.objectweb.asm.*;
import org.codehaus.aspectwerkz.transform.Context;
import org.codehaus.aspectwerkz.transform.TransformationUtil;
import org.codehaus.aspectwerkz.joinpoint.management.JoinPointType;
import org.codehaus.aspectwerkz.joinpoint.management.JoinPointMetaData;
import org.codehaus.aspectwerkz.definition.SystemDefinition;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.expression.PointcutType;
import org.codehaus.aspectwerkz.AspectSystem;
import org.codehaus.aspectwerkz.SystemLoader;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;

/**
 * Adds a "proxy method" to the methods that matches an <tt>execution</tt> pointcut as well as prefixing the "original
 * method".
 * 
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 */
public class MethodExecutionClassAdapter extends ClassAdapter implements Constants, TransformationConstants {

    private final ClassLoader m_loader;

    private final AspectSystem m_system;

    private final ClassInfo m_classInfo;

    private final ContextImpl m_ctx;

    private String m_declaringType;

    private int m_sequence = 1;

    /**
     * Creates a new class adapter.
     * 
     * @param cv
     * @param loader
     * @param classInfo
     * @param ctx
     */
    public MethodExecutionClassAdapter(final ClassVisitor cv,
                                   final ClassLoader loader,
                                   final ClassInfo classInfo,
                                   final Context ctx) {
        super(cv);
        m_loader = loader;
        m_classInfo = classInfo;
        m_ctx = (ContextImpl) ctx;
        m_system = SystemLoader.getSystem(m_loader);
        m_system.initialize();
    }

    /**
     * Visits the class.
     * 
     * @see org.objectweb.asm.ClassVisitor#visit(int, java.lang.String, java.lang.String, java.lang.String[],
     *      java.lang.String)
     */
    public void visit(
        final int access,
        final String name,
        final String superName,
        final String[] interfaces,
        final String sourceFile) {
        m_declaringType = name;
        super.visit(access, name, superName, interfaces, sourceFile);
    }

    /**
     * Visits the methods.
     * 
     * @see org.objectweb.asm.ClassVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String[],
     *      org.objectweb.asm.Attribute)
     */
    public CodeVisitor visitMethod(
        final int access,
        final String name,
        final String desc,
        final String[] exceptions,
        final Attribute attrs) {

        if (INIT_METHOD_NAME.equals(name) || CLINIT_METHOD_NAME.equals(name)) {
            return cv.visitMethod(access, name, desc, exceptions, attrs);
        }

        int hash = AsmHelper.calculateMethodHash(name, desc);
        MethodInfo methodInfo = m_classInfo.getMethod(hash);
        if (methodInfo == null) {
            throw new Error("method info metadata structure could not be build for method: " + name + " " + desc);
        }

        ExpressionContext ctx = new ExpressionContext(PointcutType.EXECUTION, methodInfo, null);

        if (methodFilter(m_ctx.getDefinitions(), ctx, methodInfo)) {
            return cv.visitMethod(access, name, desc, exceptions, attrs);

        } else {
            m_sequence++; // single place of incrementation, is used in multiple places
            m_ctx.markAsAdvised();

            // create the proxy for the original method
            createProxyMethod(access, name, desc, exceptions, attrs, methodInfo);

            // prefix the original method
            return cv.visitMethod(access,
            // TODO relies on the fact that the methods are visited in the same order each time,
                // need verification
                TransformationUtil.getPrefixedMethodName(name, m_sequence, m_declaringType), desc, exceptions, attrs);
        }
    }

    /**
     * Creates the "proxy method", e.g. the method that has the same name and signature as the original method but a
     * completely other implementation.
     * 
     * @param access
     * @param name
     * @param desc
     * @param exceptions
     * @param attrs
     */
    private void createProxyMethod(
        final int access,
        final String name,
        final String desc,
        final String[] exceptions,
        final Attribute attrs,
        final MethodInfo methodInfo) {
        
        // FIXME copy the annotations from original method to this method

        CodeVisitor mv = cv.visitMethod(access, name, desc, exceptions, attrs);
        AsmHelper.loadArgumentTypes(mv, Type.getArgumentTypes(desc), Modifier.isStatic(access));

//        JoinPointMetaData metaData = JoinPointMetaData.getJoinPointMetaData(
//            PointcutType.EXECUTION,
//            m_system,
//            methodInfo,
//            null);

        int joinPointHash = AsmHelper.calculateMethodHash(name, desc);
        String joinPointClassName = JoinPointCompiler.getJoinPointClassName(
            m_declaringType,
            JoinPointType.METHOD_EXECUTION,
            joinPointHash);

        //TODO: should we provide some sort of option to do JITgen when weaving instead of when loading ?
        // use case: offline full packaging and alike
        //        Class joinPoint = JoinPointCompiler.loadJoinPoint(
        //            JoinPointType.METHOD_EXECUTION,
        //            AsmHelper.calculateMethodHash(name, desc),
        //            name,
        //            desc,
        //            access,
        //            m_declaringType,
        //            metaData.adviceIndexes,
        //            m_loader,
        //            m_sequence);

        if (Modifier.isStatic(access)) {
            mv.visitInsn(ACONST_NULL);
        } else {
            mv.visitVarInsn(ALOAD, 0);
        }

        StringBuffer sig = new StringBuffer();
        int index = desc.lastIndexOf(')');
        sig.append(desc.substring(0, index));
        sig.append('L');
        sig.append(m_declaringType);
        sig.append(';');
        sig.append(desc.substring(index, desc.length()));
        mv.visitMethodInsn(INVOKESTATIC, joinPointClassName, "invoke", sig.toString());

        AsmHelper.addReturnStatement(mv, Type.getReturnType(desc));
        mv.visitMaxs(0, 0);

        // emit the joinpoint
        m_ctx.addEmittedInlinedJoinPoint(new ContextImpl.EmittedInlinedJoinPoint(
            JoinPointType.METHOD_EXECUTION,
            m_declaringType,
            name,
            desc,
            access,
            null,
            null,
            null,
            m_sequence,
            joinPointHash,
            joinPointClassName));
    }

    /**
     * Filters out the methods that are not eligible for transformation.
     * 
     * @param definitions
     * @param ctx
     * @param method
     * @return boolean true if the method should be filtered out
     */
    public static boolean methodFilter(final List definitions, final ExpressionContext ctx, final MethodInfo methodInfo) {
        if (Modifier.isAbstract(methodInfo.getModifiers())
            || Modifier.isNative(methodInfo.getModifiers())
            || methodInfo.getName().equals(INIT_METHOD_NAME)
            || methodInfo.getName().equals(CLINIT_METHOD_NAME)
            || methodInfo.getName().startsWith(TransformationUtil.ORIGINAL_METHOD_PREFIX)) {
            return true;
        }
        for (Iterator it = definitions.iterator(); it.hasNext();) {
            if (((SystemDefinition) it.next()).hasPointcut(ctx)) {
                return false;
            } else {
                continue;
            }
        }
        return true;
    }
}