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

import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Unknown;
import org.apache.bcel.generic.Type;

import java.util.*;
import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.Field;

import org.aspectj.weaver.AjAttribute;
import org.aspectj.weaver.ISourceContext;
import org.aspectj.weaver.patterns.Pointcut;
import org.aspectj.weaver.patterns.KindedPointcut;
import org.aspectj.weaver.patterns.SignaturePattern;

import org.codehaus.aspectwerkz.exception.WrappedRuntimeException;
import org.codehaus.aspectwerkz.exception.DefinitionException;
import org.codehaus.aspectwerkz.aspect.AdviceType;
import org.codehaus.aspectwerkz.definition.AspectDefinition;
import org.codehaus.aspectwerkz.definition.DefinitionParserHelper;
import org.codehaus.aspectwerkz.definition.AdviceDefinition;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.transform.inlining.spi.AspectModel;
import org.codehaus.aspectwerkz.transform.inlining.AdviceMethodInfo;
import org.codehaus.aspectwerkz.transform.inlining.AspectInfo;
import org.codehaus.aspectwerkz.transform.inlining.compiler.AbstractJoinPointCompiler;
import org.codehaus.aspectwerkz.transform.TransformationConstants;

import org.codehaus.aspectwerkz.org.objectweb.asm.ClassWriter;
import org.codehaus.aspectwerkz.org.objectweb.asm.CodeVisitor;

/**
 * Implementation of the AspectModel interface for the AspectJ framework.
 * <p/>
 * Provides methods for definition of aspects and framework specific bytecode generation
 * used by the join point compiler.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 */
public class AspectJAspectModel implements AspectModel, TransformationConstants {

    private static final String ASPECT_MODEL_TYPE = "aspectj";
    private static final String ASPECTJ_AROUND_CLOSURE_CLASS_NAME = "org/aspectj/runtime/internal/AroundClosure";
    private static final String ASPECTJ_AROUND_CLOSURE_RUN_METHOD_NAME = "run";
    private static final String ASPECTJ_AROUND_CLOSURE_RUN_METHOD_SIGNATURE = "([Ljava/lang/Object;)Ljava/lang/Object;";
    private static final String PREFIX_AROUND_ADVICE = "ajc$around$";
    private static final String PREFIX_BEFORE_ADVICE = "ajc$before$";
    private static final String PREFIX_AFTER_FINALLY_ADVICE = "ajc$after$";
    private static final String PREFIX_AFTER_THROWING_ADVICE = "ajc$afterThrowing$";
    private static final String PREFIX_AFTER_RETURNING_ADVICE = "ajc$afterReturning$";
    private static final String PROCEED_SUFFIX = "proceed";

    /**
     * Returns the aspect model type, which is an id for the the special aspect model, can be anything as long
     * as it is unique.
     *
     * @return the aspect model type id
     */
    public String getAspectModelType() {
        return ASPECT_MODEL_TYPE;
    }

    /**
     * Defines the aspect.
     *
     * @param classInfo
     * @param aspectDef
     * @param loader
     */
    public void defineAspect(final ClassInfo classInfo,
                             final AspectDefinition aspectDef,
                             final ClassLoader loader) {

        JavaClass javaClass = null;
        String classFileName = classInfo.getName().replace('.', '/') + ".class";
        try {
            InputStream classStream = loader.getResourceAsStream(classFileName);
            ClassParser classParser = new ClassParser(classStream, classFileName);
            javaClass = classParser.parse();
        } catch (IOException e) {
            throw new WrappedRuntimeException(e);
        }

        List attributes = getAspectAttributes(javaClass);
        if (attributes.size() == 0) {
            return; // not an AspectJ aspect
        }

        aspectDef.setAspectModel(getAspectModelType());

        for (Iterator it = attributes.iterator(); it.hasNext();) {
            AjAttribute attr = (AjAttribute) it.next();
            if (attr instanceof AjAttribute.PointcutDeclarationAttribute) {
                AjAttribute.PointcutDeclarationAttribute pcAttr = (AjAttribute.PointcutDeclarationAttribute) attr;
                Pointcut pointcut = pcAttr.reify().getPointcut();
                if (pointcut instanceof KindedPointcut) {
                    try {
                        Field sigField = KindedPointcut.class.getDeclaredField("signature");
                        sigField.setAccessible(true);
                        SignaturePattern signature = (SignaturePattern) sigField.get(pointcut);
                        DefinitionParserHelper.createAndAddPointcutDefToAspectDef(
                                signature.getName().toString(), pointcut.toString(), aspectDef
                        );
                    } catch (Exception e) {
                        throw new WrappedRuntimeException(e);
                    }
                }
            }
        }
        for (Iterator iterator = getAroundAdviceInfo(javaClass).iterator(); iterator.hasNext();) {
            AdviceInfo ajAdviceInfo = (AdviceInfo) iterator.next();
            AdviceDefinition adviceDef = handleAdviceInfo(classInfo, aspectDef, ajAdviceInfo);
            aspectDef.addAroundAdviceDefinition(adviceDef);
        }
        for (Iterator iterator = getBeforeAdviceInfo(javaClass).iterator(); iterator.hasNext();) {
            AdviceInfo ajAdviceInfo = (AdviceInfo) iterator.next();
            AdviceDefinition adviceDef = handleAdviceInfo(classInfo, aspectDef, ajAdviceInfo);
            aspectDef.addBeforeAdviceDefinition(adviceDef);
        }
        for (Iterator iterator = getAfterFinallyAdviceInfo(javaClass).iterator(); iterator.hasNext();) {
            AdviceInfo ajAdviceInfo = (AdviceInfo) iterator.next();
            AdviceDefinition adviceDef = handleAdviceInfo(classInfo, aspectDef, ajAdviceInfo);
            aspectDef.addAfterAdviceDefinition(adviceDef);
        }
        for (Iterator iterator = getAfterReturningAdviceInfo(javaClass).iterator(); iterator.hasNext();) {
            AdviceInfo ajAdviceInfo = (AdviceInfo) iterator.next();
            AdviceDefinition adviceDef = handleAdviceInfo(classInfo, aspectDef, ajAdviceInfo);
            aspectDef.addAfterAdviceDefinition(adviceDef);
        }
        for (Iterator iterator = getAfterThrowingAdviceInfo(javaClass).iterator(); iterator.hasNext();) {
            AdviceInfo ajAdviceInfo = (AdviceInfo) iterator.next();
            AdviceDefinition adviceDef = handleAdviceInfo(classInfo, aspectDef, ajAdviceInfo);
            aspectDef.addAfterAdviceDefinition(adviceDef);
        }
    }

    /**
     * AspectJ is not in need for reflective information, passes contextual info through args() binding etc.
     * or handles it itself using 'thisJoinPoint'.
     *
     * @return true
     */
    public boolean requiresReflectiveInfo() {
        return false;
    }

    /**
     * Returns info about the closure class, name and type (interface or class).
     *
     * @return the closure class info
     */
    public AroundClosureClassInfo getAroundClosureClassInfo() {
        return new AspectModel.AroundClosureClassInfo( ASPECTJ_AROUND_CLOSURE_CLASS_NAME, new String[]{});
    }


    /**
     * Creates the methods required to implement or extend to implement the closure for the specific aspect model type.
     *
     * @param cw
     * @param className
     */
    public void createMandatoryMethods(final ClassWriter cw, final String className) {
        CodeVisitor cv = cw.visitMethod(
                ACC_PUBLIC, ASPECTJ_AROUND_CLOSURE_RUN_METHOD_NAME,
                ASPECTJ_AROUND_CLOSURE_RUN_METHOD_SIGNATURE,
                new String[]{THROWABLE_CLASS_NAME},
                null
        );
        cv.visitVarInsn(ALOAD, 0);
        cv.visitMethodInsn(INVOKEVIRTUAL, className, PROCEED_METHOD_NAME, PROCEED_METHOD_SIGNATURE);
        cv.visitInsn(ARETURN);
        cv.visitMaxs(0, 0);
    }

    /**
     * Creates an invocation of the around closure class' constructor.
     *
     * @param cv
     */
    public void createInvocationOfAroundClosureSuperClass(final CodeVisitor cv) {
        cv.visitInsn(ACONST_NULL);
        cv.visitMethodInsn(
                INVOKESPECIAL,
                ASPECTJ_AROUND_CLOSURE_CLASS_NAME,
                INIT_METHOD_NAME,
                "([Ljava/lang/Object;)V"
        );
    }

    /**
     * Creates a field to host the aspectj aspect instance
     * <p/>
     * TODO support other aspect deployment model
     *
     * @param cw
     * @param aspectInfo
     * @param joinPointClassName
     */
    public void createAspectReferenceField(final ClassWriter cw,
                                           final AspectInfo aspectInfo,
                                           final String joinPointClassName) {
        AbstractJoinPointCompiler.createAspectReferenceField(cw, aspectInfo);
    }

    /**
     * Creates instantiation of the aspectj aspect instance by invoking aspectOf().
     * <p/>
     * TODO support other aspectOf() types of aspect retrieval
     *
     * @param cv
     * @param aspectInfo
     * @param joinPointClassName
     */
    public void createAspectInstantiation(final CodeVisitor cv,
                                          final AspectInfo aspectInfo,
                                          final String joinPointClassName) {
        cv.visitMethodInsn(
                INVOKESTATIC,
                aspectInfo.getAspectClassName(),
                "aspectOf()",
                "()" + aspectInfo.getAspectClassSignature()
        );
        // no cast needed
        cv.visitFieldInsn(
                PUTSTATIC, joinPointClassName, aspectInfo.getAspectFieldName(), aspectInfo.getAspectClassSignature()
        );

    }

    /**
     * Handles the arguments to the before around.
     *
     * @param cv
     * @param adviceMethodInfo
     */
    public void createAroundAdviceArgumentHandling(final CodeVisitor cv, final AdviceMethodInfo adviceMethodInfo) {
    }

    /**
     * Handles the arguments to the before advice.
     *
     * @param cv
     * @param adviceMethodInfo
     */
    public void createBeforeAdviceArgumentHandling(final CodeVisitor cv, final AdviceMethodInfo adviceMethodInfo) {
    }

    /**
     * Handles the arguments to the after advice.
     *
     * @param cv
     * @param adviceMethodInfo
     */
    public void createAfterAdviceArgumentHandling(final CodeVisitor cv, final AdviceMethodInfo adviceMethodInfo) {
        final AdviceType adviceType = adviceMethodInfo.getAdviceInfo().getType();
        final int specialArgumentIndex = adviceMethodInfo.getSpecialArgumentIndex();
        final String specialArgumentTypeName = adviceMethodInfo.getSpecialArgumentTypeName();
        if (adviceType.equals(AdviceType.AFTER_RETURNING) ||
            adviceType.equals(AdviceType.AFTER_THROWING)) {
            cv.visitVarInsn(ALOAD, specialArgumentIndex);
            cv.visitTypeInsn(CHECKCAST, specialArgumentTypeName);
        }
    }

    /**
     * Handles the AspectJ advice info.
     *
     * @param classInfo
     * @param aspectDef
     * @param ajAdviceInfo
     * @return the new advice definition
     */
    private AdviceDefinition handleAdviceInfo(final ClassInfo classInfo,
                                              final AspectDefinition aspectDef,
                                              final AdviceInfo ajAdviceInfo) {

        MethodInfo adviceMethod = null;
        MethodInfo[] methods = classInfo.getMethods();
        for (int i = 0; i < methods.length; i++) {
            MethodInfo method = methods[i];
            if (method.getName().equals(ajAdviceInfo.adviceMethodName)) {
                adviceMethod = method;
                break;
            }
        }
        if (adviceMethod == null) {
            throw new Error(
                    "advice method [" + ajAdviceInfo.adviceMethodName +
                    "] could not be found in class [" + classInfo.getName() + "]"
            );
        }
        String specialArgType = null;
        if (ajAdviceInfo.extraParameterFlags != 0) {
            specialArgType = ajAdviceInfo.parameterTypes[0];
        }
        return AdviceDefinition.newInstance(
                createFullAspectJAdviceMethodName(ajAdviceInfo),
                ajAdviceInfo.type,
                ajAdviceInfo.pointcut,
                specialArgType,
                classInfo.getName(),
                classInfo.getName(),
                adviceMethod,
                aspectDef
        );
    }

    /**
     * Creates a full AspectJ advice method name.
     *
     * @param ajAdviceInfo
     * @return the method name
     */
    private String createFullAspectJAdviceMethodName(final AdviceInfo ajAdviceInfo) {
        StringBuffer fullAdviceMethodName = new StringBuffer();
        fullAdviceMethodName.append(ajAdviceInfo.adviceMethodName).append('(');

        // FIXME support args() target() and this()
//        for (int i = 0; i < ajAdviceInfo.parameterTypes.length; i++) {
//            String type = ajAdviceInfo.parameterTypes[i];
//            fullAdviceMethodName.append(type).append(" arg").append(i);
//            if (i < ajAdviceInfo.parameterTypes.length - 1) {
//                fullAdviceMethodName.append(',');
//            }
//        }
        fullAdviceMethodName.append(')');
        return fullAdviceMethodName.toString();
    }

    /**
     * Returns the around advice infos.
     *
     * @param javaClass
     * @return
     */
    private List getAroundAdviceInfo(final JavaClass javaClass) {
        List advice = new ArrayList();
        Method[] methods = javaClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            String adviceName = method.getName();
            if (adviceName.startsWith(PREFIX_AROUND_ADVICE) && !adviceName.endsWith(PROCEED_SUFFIX)) {
                advice.add(createAdviceInfo(javaClass, AdviceType.AROUND, method));
            }
        }
        return advice;
    }

    /**
     * Returns the before advice infos.
     *
     * @param javaClass
     * @return
     */
    private List getBeforeAdviceInfo(final JavaClass javaClass) {
        List advice = new ArrayList();
        Method[] methods = javaClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            String adviceName = method.getName();
            if (adviceName.startsWith(PREFIX_BEFORE_ADVICE)) {
                advice.add(createAdviceInfo(javaClass, AdviceType.BEFORE, method));
            }
        }
        return advice;
    }

    /**
     * Returns the around advice infos.
     *
     * @param javaClass
     * @return
     */
    private List getAfterFinallyAdviceInfo(final JavaClass javaClass) {
        List advice = new ArrayList();
        Method[] methods = javaClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            String adviceName = method.getName();
            if (adviceName.startsWith(PREFIX_AFTER_FINALLY_ADVICE)) {
                advice.add(createAdviceInfo(javaClass, AdviceType.AFTER_FINALLY, method));
            }
        }
        return advice;
    }

    /**
     * Returns the after throwing advice infos.
     *
     * @param javaClass
     * @return
     */
    private List getAfterThrowingAdviceInfo(final JavaClass javaClass) {
        List advice = new ArrayList();
        Method[] methods = javaClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            String adviceName = method.getName();
            if (adviceName.startsWith(PREFIX_AFTER_THROWING_ADVICE)) {
                advice.add(createAdviceInfo(javaClass, AdviceType.AFTER_THROWING, method));
            }
        }
        return advice;
    }

    /**
     * Returns the after returning advice infos.
     *
     * @param javaClass
     * @return
     */
    private List getAfterReturningAdviceInfo(final JavaClass javaClass) {
        List advice = new ArrayList();
        Method[] methods = javaClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            String adviceName = method.getName();
            if (adviceName.startsWith(PREFIX_AFTER_RETURNING_ADVICE)) {
                advice.add(createAdviceInfo(javaClass, AdviceType.AFTER_RETURNING, method));
            }
        }
        return advice;
    }

    /**
     * Creates advice info for the advice method.
     *
     * @param adviceType
     * @param method
     * @return
     */
    private AdviceInfo createAdviceInfo(final JavaClass javaClassfinal,
                                        final AdviceType adviceType,
                                        final Method method) {
        Attribute[] attributes = method.getAttributes();
        List ajAttributes = readAjAttributes(attributes, null);
        AdviceInfo adviceInfo = new AdviceInfo();
        for (Iterator it = ajAttributes.iterator(); it.hasNext();) {
            AjAttribute attr = (AjAttribute) it.next();
            if (attr instanceof AjAttribute.AdviceAttribute) {
                AjAttribute.AdviceAttribute adviceAttr = (AjAttribute.AdviceAttribute) attr;
                adviceInfo.type = adviceType;
                adviceInfo.aspectClassName = javaClassfinal.getClassName().replace('.', '/');
                adviceInfo.adviceMethodName = method.getName();
                String pointcut = adviceAttr.getPointcut().toString();
                if (pointcut.startsWith("execution(") ||
                        pointcut.startsWith("call(") ||
                        pointcut.startsWith("set(") ||
                        pointcut.startsWith("get(") ||
                        pointcut.startsWith("handler(") ||
                        pointcut.startsWith("adviceexecution(") ||
                        pointcut.startsWith("within(") ||
                        pointcut.startsWith("withincode(") ||
                        pointcut.startsWith("cflow(") ||
                        pointcut.startsWith("cflowbelow(") ||
                        pointcut.startsWith("if(") ||
                        pointcut.startsWith("this(") ||
                        pointcut.startsWith("target(") ||
                        pointcut.startsWith("args(") ||
                        pointcut.startsWith("initialization(") ||
                        pointcut.startsWith("staticinitialization(") ||
                        pointcut.startsWith("preinitialization(")) {
                    adviceInfo.pointcut = pointcut;
                } else if (pointcut.endsWith("()")) {
                    adviceInfo.pointcut = pointcut.substring(0, pointcut.length() - 2);
                } else {
                    throw new DefinitionException("pointcuts of type [" + pointcut + " are not yet supported");
                }
                adviceInfo.extraParameterFlags = adviceAttr.getExtraParameterFlags();
                int nrArgs = method.getArgumentTypes().length;
                String[] parameterTypes = new String[nrArgs];
                for (int i = 0; i < nrArgs; i++) {
                    Type type = method.getArgumentTypes()[i];
                    parameterTypes[i] = type.toString().replace('.', '/');
                }
                adviceInfo.parameterTypes = parameterTypes;
            }
        }
        return adviceInfo;
    }

    /**
     * Returns the aspect attributes.
     *
     * @param javaClass
     * @return
     */
    private List getAspectAttributes(final JavaClass javaClass) {
        return readAjAttributes(javaClass.getAttributes(), null);
    }

    /**
     * Reads in the AjAttributes from the bytecode.
     *
     * @param attrs
     * @param context
     * @return
     */
    private static List readAjAttributes(final Attribute[] attrs, final ISourceContext context) {
        List ajAttrs = new ArrayList();
        for (int i = attrs.length - 1; i >= 0; i--) {
            Attribute a = attrs[i];
            if (a instanceof Unknown) {
                Unknown u = (Unknown) a;
                String name = u.getName();
                if (name.startsWith(AjAttribute.AttributePrefix)) {
                    ajAttrs.add(AjAttribute.read(name, u.getBytes(), context));
                }
            }
        }
        return ajAttrs;
    }
}
