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

import gnu.trove.TObjectIntHashMap;
import org.codehaus.aspectwerkz.definition.SystemDefinition;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.expression.PointcutType;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.reflect.impl.javassist.JavassistClassInfo;
import org.codehaus.aspectwerkz.reflect.impl.javassist.JavassistMethodInfo;
import org.codehaus.aspectwerkz.transform.Context;
import org.codehaus.aspectwerkz.transform.TransformationUtil;
import org.codehaus.aspectwerkz.transform.Transformer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;
import javassist.NotFoundException;

/**
 * Advises method EXECUTION join points.
 * 
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
 */
public class MethodExecutionTransformer implements Transformer {

    /**
     * Makes the member method transformations.
     * 
     * @param context the transformation context
     * @param klass the class set.
     */
    public void transform(final Context context, final Klass klass) throws Exception {
        List definitions = context.getDefinitions();

        //m_joinPointIndex =
        // TransformationUtil.getJoinPointIndex(klass.getCtClass()); // TODO not
        // thread safe

        final CtClass ctClass = klass.getCtClass();
        ClassInfo classInfo = JavassistClassInfo.getClassInfo(ctClass, context.getLoader());
        
        if (classFilter(definitions, new ExpressionContext(PointcutType.EXECUTION, classInfo, null), ctClass)) {
            return;
        }
        final CtMethod[] methods = ctClass.getDeclaredMethods();

        // Compute the method sequence number no matter the method is advised to
        // support multi weaving.
        // Javassist.getDeclaredMethods() does not always return methods in the
        // same order so we have
        // to sort the method list before computation.
        // @TODO: filter init/clinit/prefixed methods
        final List sortedMethods = Arrays.asList(methods);
        Collections.sort(sortedMethods, JavassistMethodComparator.getInstance());
        final TObjectIntHashMap methodSequences = new TObjectIntHashMap();
        final List sortedMethodTuples = new ArrayList(sortedMethods.size());
        for (Iterator methodsIt = sortedMethods.iterator(); methodsIt.hasNext();) {
            CtMethod method = (CtMethod) methodsIt.next();
            MethodInfo methodInfo = JavassistMethodInfo.getMethodInfo(method, context.getLoader());
            int sequence = 1;
            if (methodSequences.containsKey(method.getName())) {
                sequence = methodSequences.get(method.getName());
                methodSequences.remove(method.getName());
                sequence++;
            }
            methodSequences.put(method.getName(), sequence);
            ExpressionContext ctx = new ExpressionContext(PointcutType.EXECUTION, methodInfo, null);
            MethodSequenceTuple tuple = new MethodSequenceTuple(method, sequence);
            int status = methodFilter(definitions, ctx, method);
            tuple.setStatus(status);

            // @TODO filter out "skip" status
            sortedMethodTuples.add(tuple);
        }
        final List wrapperMethods = new ArrayList();
        boolean isClassAdvised = false;
        for (Iterator i = sortedMethodTuples.iterator(); i.hasNext();) {
            MethodSequenceTuple tuple = (MethodSequenceTuple) i.next();
            if (tuple.getStatus() != STATUS_HAS_POINTCUT) {
                continue;
            }
            CtMethod method = tuple.getMethod();
            final int methodSequence = tuple.getSequence();
            final int methodHash = JavassistHelper.calculateHash(method);

            // there was no empty method already
            final String prefixedMethodName = TransformationUtil.getPrefixedMethodName(
                method.getName(),
                methodSequence,
                ctClass.getName().replace('/', '.'));
            if (JavassistHelper.hasMethod(ctClass, prefixedMethodName)) {
                CtMethod wrapperMethod = ctClass.getDeclaredMethod(prefixedMethodName);
                if (JavassistHelper.isAnnotatedEmpty(wrapperMethod)) {
                    // create the non empty wrapper to access its body
                    CtMethod nonEmptyWrapper = createWrapperMethod(ctClass, method, methodHash, klass);
                    wrapperMethod.setBody(method, null);
                    method.setBody(nonEmptyWrapper, null);
                    JavassistHelper.setAnnotatedNotEmpty(wrapperMethod);
                    isClassAdvised = true;
                } else {
                    // multi weaving
                    continue;
                }
            } else {
                // new execution pointcut
                CtMethod wrapperMethod = createWrapperMethod(ctClass, method, methodHash, klass);
                wrapperMethods.add(wrapperMethod);
                addPrefixToMethod(ctClass, method, methodSequence);
                isClassAdvised = true;
            }
        }
        if (isClassAdvised) {
            context.markAsAdvised();

            // add the wrapper methods
            for (Iterator it2 = wrapperMethods.iterator(); it2.hasNext();) {
                ctClass.addMethod((CtMethod) it2.next());
            }
        }

        // handles pointcut unweaving
        // looping on the original methods is enough since we will look for
        // method with no pc
        // thus that have not been changed in the previous transformation steps
        for (Iterator i = sortedMethodTuples.iterator(); i.hasNext();) {
            MethodSequenceTuple tuple = (MethodSequenceTuple) i.next();

            //System.out.println(" tuple " + tuple.getAdvice().getName() + " :
            // " + tuple.getStatus());
            if (tuple.getStatus() != STATUS_HAS_NO_POINTCUT) {
                continue;
            }
            CtMethod method = tuple.getMethod();

            //System.out.println("FOUND NO PC = " + method.getName());
            final String prefixedMethodName = TransformationUtil.getPrefixedMethodName(method.getName(), tuple
                    .getSequence(), ctClass.getName().replace('/', '.'));

            // do we have a wrapper method, which is NOT marked empty
            if (JavassistHelper.hasMethod(ctClass, prefixedMethodName)) {
                CtMethod wrapperMethod = ctClass.getDeclaredMethod(prefixedMethodName);
                if (JavassistHelper.isAnnotatedNotEmpty(wrapperMethod)) {
                    //System.out.println("FOUND A real Wrapper but NO PC = " +
                    // method.getName());
                    CtMethod emptyWrapperMethod = JavassistHelper.createEmptyWrapperMethod(ctClass, method, tuple
                            .getSequence());
                    method.setBody(wrapperMethod, null);
                    wrapperMethod.setBody(emptyWrapperMethod, null);
                    JavassistHelper.setAnnotatedEmpty(wrapperMethod);
                    context.markAsAdvised();
                }
            }
        }

        //}//end of def loop
        //TransformationUtil.setJoinPointIndex(klass.getCtClass(),
        // m_joinPointIndex);
        klass.flushJoinPointIndex();
    }

    /**
     * Creates a wrapper method for the original method specified. This method has the same signature as the original
     * method and catches the invocation for further processing by the framework before redirecting to the original
     * method.
     * 
     * @param ctClass the ClassGen
     * @param originalMethod the current method
     * @param methodHash the method hash
     * @return the wrapper method
     */
    private CtMethod createWrapperMethod(
        final CtClass ctClass,
        final CtMethod originalMethod,
        final int methodHash,
        final Klass klass) throws NotFoundException, CannotCompileException {
        StringBuffer body = new StringBuffer();
        StringBuffer callBody = new StringBuffer();
        body.append('{');
        callBody.append(TransformationUtil.JOIN_POINT_MANAGER_FIELD);
        callBody.append('.');
        callBody.append(TransformationUtil.PROCEED_WITH_EXECUTION_JOIN_POINT_METHOD);
        callBody.append('(');
        callBody.append(methodHash);
        callBody.append(", ");
        callBody.append(klass.getJoinPointIndex());
        callBody.append(", args, ");
        if (Modifier.isStatic(originalMethod.getModifiers())) {
            callBody.append("nullObject");
            body.append("Object nullObject = null;");
        } else {
            callBody.append("this");
        }
        callBody.append(',');
        callBody.append(TransformationUtil.JOIN_POINT_TYPE_METHOD_EXECUTION);
        callBody.append(");");
        if (originalMethod.getParameterTypes().length > 0) {
            body.append("Object[] args = $args; ");
        } else {
            body.append("Object[] args = null; ");
        }
        if (originalMethod.getReturnType() == CtClass.voidType) {
            // special handling for void return type leads to cleaner bytecode
            // generation with Javassist
            body.append(callBody.toString()).append("}");
        } else if (!originalMethod.getReturnType().isPrimitive()) {
            body.append("return ($r)");
            body.append(callBody.toString());
            body.append("}");
        } else {
            String localResult = TransformationUtil.ASPECTWERKZ_PREFIX + "res";
            body.append("Object ").append(localResult).append(" = ");
            body.append(callBody.toString());
            body.append("if (").append(localResult).append(" != null)");
            body.append("return ($r) ").append(localResult).append("; else ");
            body.append("return ");
            body.append(JavassistHelper.getDefaultPrimitiveValue(originalMethod.getReturnType()));
            body.append("; }");
        }
        CtMethod method;
        if (Modifier.isStatic(originalMethod.getModifiers())) {
            method = JavassistHelper.makeStatic(
                originalMethod.getReturnType(),
                originalMethod.getName(),
                originalMethod.getParameterTypes(),
                originalMethod.getExceptionTypes(),
                body.toString(),
                ctClass);
        } else {
            method = CtNewMethod.make(originalMethod.getReturnType(), originalMethod.getName(), originalMethod
                    .getParameterTypes(), originalMethod.getExceptionTypes(), body.toString(), ctClass);
            method.setModifiers(originalMethod.getModifiers());
        }
        JavassistHelper.copyCustomAttributes(method, originalMethod);
        klass.incrementJoinPointIndex();
        JavassistHelper.setAnnotatedNotEmpty(method);
        return method;
    }

    /**
     * Adds a prefix to the original method. To make it callable only from within the framework itself.
     * 
     * @param cg class gen
     * @param ctMethod the current method
     * @param methodSequence the methods sequence number
     */
    private void addPrefixToMethod(final CtClass cg, final CtMethod ctMethod, final int methodSequence) {
        // change the method access flags (should always be set to protected)
        int accessFlags = ctMethod.getModifiers();
        String prefixedMethodName = TransformationUtil.getPrefixedMethodName(ctMethod.getName(), methodSequence, cg
                .getName());
        ctMethod.setName(prefixedMethodName);
        ctMethod.setModifiers(accessFlags);
    }

    /**
     * Filters the classes to be transformed.
     * 
     * @param definitions the definitions
     * @param ctx the context
     * @param cg the class to filter
     * @return boolean true if the method should be filtered away
     */
    private boolean classFilter(final List definitions, final ExpressionContext ctx, final CtClass cg) {
        if (cg.isInterface()) {
            return true;
        }
        for (Iterator defs = definitions.iterator(); defs.hasNext();) {
            if (classFilter((SystemDefinition) defs.next(), ctx, cg)) {
                continue;
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * Filters the classes to be transformed. <p/>TODO: when a class had execution pointcut that were removed it must be
     * unweaved, thus not filtered out How to handle that ? cache lookup ? or custom class level attribute ?
     * 
     * @param definition the definition
     * @param ctx the context
     * @param cg the class to filter
     * @return boolean true if the method should be filtered away
     */
    public static boolean classFilter(final SystemDefinition definition, final ExpressionContext ctx, final CtClass cg) {
        if (cg.isInterface()) {
            return true;
        }
        String className = cg.getName().replace('/', '.');
        if (definition.inExcludePackage(className)) {
            return true;
        }
        if (!definition.inIncludePackage(className)) {
            return true;
        }
        if (definition.isAdvised(ctx)) {
            return false;
        }
        if (definition.inPreparePackage(className)) {
            return false; //no early filtering for prepared Class to allow RuW
        }
        return true;
    }

    /**
     * Filters the methods to be transformed.
     * 
     * @param definitions
     * @param ctx
     * @param method
     * @return
     */
    public static int methodFilter(final List definitions, final ExpressionContext ctx, final CtMethod method) {
        if (Modifier.isAbstract(method.getModifiers())
            || Modifier.isNative(method.getModifiers())
            || method.getName().equals("<init>")
            || method.getName().equals("<clinit>")
            || method.getName().startsWith(TransformationUtil.ORIGINAL_METHOD_PREFIX)
            || method.getName().equals(TransformationUtil.GET_META_DATA_METHOD)
            || method.getName().equals(TransformationUtil.SET_META_DATA_METHOD)
            || method.getName().equals(TransformationUtil.CLASS_LOOKUP_METHOD)
            || method.getName().equals(TransformationUtil.GET_UUID_METHOD)) {
            return STATUS_SKIP;
        }
        for (Iterator defs = definitions.iterator(); defs.hasNext();) {
            if (((SystemDefinition) defs.next()).hasPointcut(ctx)) {
                return STATUS_HAS_POINTCUT;
            } else {
                continue;
            }
        }
        return STATUS_HAS_NO_POINTCUT;
    }
}

/**
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
 */

class MethodSequenceTuple {
    private CtMethod m_method;

    private int m_sequence;

    private int m_status = MethodExecutionTransformer.STATUS_SKIP;

    public MethodSequenceTuple(CtMethod method, int sequence) {
        m_method = method;
        m_sequence = sequence;
    }

    public CtMethod getMethod() {
        return m_method;
    }

    public int getSequence() {
        return m_sequence;
    }

    public void setStatus(int status) {
        m_status = status;
    }

    public int getStatus() {
        return m_status;
    }

    public String toString() {
        return m_method.getName() + " : " + m_status;
    }
}