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

import java.util.Map;
import java.util.Iterator;
import java.util.WeakHashMap;
import java.util.HashSet;
import java.util.Set;

import org.codehaus.aspectwerkz.joinpoint.management.JoinPointType;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.expression.PointcutType;
import org.codehaus.aspectwerkz.expression.ExpressionInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.transform.inlining.AsmHelper;
import org.codehaus.aspectwerkz.transform.inlining.EmittedJoinPoint;

/**
 * TODO is factory a good name, now that it does so much more?
 * <p/>
 * Factory for the different join point implementations.
 * Compiles a new join point on the fly and loads the class.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 */
public class JoinPointFactory {

    /**
     * Stores the compilation infos - mapped to the last compiled join point class based on this compilation info.
     */
    private static final Map COMPILATION_INFO_REPOSITORY = new WeakHashMap();

    /**
     * Compiles and loades a join point class, one specific class for each distinct join point.
     *
     * @param model  the model for the compilation
     * @param loader the class loader that the compiled join point should live in
     * @return the compiled join point class
     */
    public static Class compileJoinPointAndAttachToClassLoader(final CompilationInfo.Model model,
                                                               final ClassLoader loader) {
        return attachToClassLoader(model.getJoinPointClassName(), loader, compileJoinPoint(model));
    }

    /**
     * Loads a join point class, one specific class for each distinct join point.
     *
     * @param joinpointClassName
     * @param loader the class loader that the compiled join point should live in
     * @param bytecode of the joinpoint
     * @return the compiled join point class
     */
    public static Class attachToClassLoader(final String joinpointClassName,
                                            final ClassLoader loader,
                                            final byte[] bytecode) {
        return AsmHelper.defineClass(loader, bytecode, joinpointClassName);
    }

    /**
     * Adds or updates a compilation info. The class key is always the first compiled join point class.
     *
     * @param clazz
     * @param compilationInfo
     */
    public static void addCompilationInfo(final Class clazz, final CompilationInfo compilationInfo) {
        COMPILATION_INFO_REPOSITORY.put(clazz, compilationInfo);
    }

    /**
     * Compiles a join point class, one specific class for each distinct join point.
     *
     * @param model the model for the compilation
     * @return the compiled join point bytecode
     */
    public static byte[] compileJoinPoint(final CompilationInfo.Model model) {
        switch (model.getEmittedJoinPoint().getJoinPointType()) {
            case JoinPointType.METHOD_EXECUTION_INT:
                return new MethodExecutionJoinPointCompiler(model).compile();
            case JoinPointType.METHOD_CALL_INT:
                return new MethodCallJoinPointCompiler(model).compile();
            case JoinPointType.CONSTRUCTOR_EXECUTION_INT:
                return new ConstructorExecutionJoinPointCompiler(model).compile();
            case JoinPointType.CONSTRUCTOR_CALL_INT:
                return new ConstructorCallJoinPointCompiler(model).compile();
            case JoinPointType.FIELD_SET_INT:
                return new FieldSetJoinPointCompiler(model).compile();
            case JoinPointType.FIELD_GET_INT:
                return new FieldGetJoinPointCompiler(model).compile();
            case JoinPointType.HANDLER_INT:
                return new HandlerJoinPointCompiler(model).compile();
            case JoinPointType.STATIC_INITIALIZATION_INT:
                return new StaticInitializationJoinPointCompiler(model).compile();
            default:
                throw new UnsupportedOperationException(
                        "join point type is not supported: " + model.getEmittedJoinPoint().getJoinPointType()
                );
        }
    }

    /**
     * Redefines the originally compiled join point.
     *
     * @param compilationInfo the model for the compilation
     * @return the compiled join point bytecode
     */
    public static byte[] redefineJoinPoint(final CompilationInfo compilationInfo) {
        switch (compilationInfo.getInitialModel().getEmittedJoinPoint().getJoinPointType()) {
            case JoinPointType.METHOD_EXECUTION_INT:
                return new MethodExecutionJoinPointRedefiner(compilationInfo).compile();
            case JoinPointType.METHOD_CALL_INT:
                return new MethodCallJoinPointRedefiner(compilationInfo).compile();
            case JoinPointType.CONSTRUCTOR_EXECUTION_INT:
                return new ConstructorExecutionJoinPointRedefiner(compilationInfo).compile();
            case JoinPointType.CONSTRUCTOR_CALL_INT:
                return new ConstructorCallJoinPointRedefiner(compilationInfo).compile();
            case JoinPointType.FIELD_SET_INT:
                return new FieldSetJoinPointRedefiner(compilationInfo).compile();
            case JoinPointType.FIELD_GET_INT:
                return new FieldGetJoinPointRedefiner(compilationInfo).compile();
            case JoinPointType.HANDLER_INT:
                return new HandlerJoinPointRedefiner(compilationInfo).compile();
            default:
                throw new UnsupportedOperationException(
                        "join point type is not supported: " +
                        compilationInfo.getInitialModel().getEmittedJoinPoint().getJoinPointType()
                );
        }
    }

    /**
     * Returns a list with all the join point compilers that matches a specific pointcut expression.
     * <p/>
     * To be used for redefinition of the join point compilers only. This since the compilers must have been created
     * in advance to exist in the repository (which is done when the target class is loaded).
     *
     * @param expression the pointcut expression
     * @return a set with the matching emitted join point
     */
    public static Set getJoinPointsMatching(final ExpressionInfo expression) {
        final Set matchingJoinPointInfos = new HashSet();
        for (Iterator it = COMPILATION_INFO_REPOSITORY.entrySet().iterator(); it.hasNext();) {
            final Map.Entry entry = (Map.Entry) it.next();

            final Class clazz = (Class) entry.getKey();
            final CompilationInfo compilationInfo = (CompilationInfo) entry.getValue();
            final EmittedJoinPoint joinPoint = (EmittedJoinPoint) compilationInfo.
                    getInitialModel().getEmittedJoinPoint();
            final ClassLoader loader = clazz.getClassLoader();

            final ClassInfo calleeClassInfo = AsmClassInfo.getClassInfo(joinPoint.getCalleeClassName(), loader);
            final ClassInfo callerClassInfo = AsmClassInfo.getClassInfo(joinPoint.getCallerClassName(), loader);
            final MethodInfo callerMethodInfo = getCallerMethodInfo(callerClassInfo, joinPoint);

            ExpressionContext ctx = null;
            switch (joinPoint.getJoinPointType()) {
                case JoinPointType.METHOD_EXECUTION_INT:
                    ctx = new ExpressionContext(
                            PointcutType.EXECUTION,
                            calleeClassInfo.getMethod(joinPoint.getJoinPointHash()),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.METHOD_CALL_INT:
                    ctx = new ExpressionContext(
                            PointcutType.CALL,
                            calleeClassInfo.getMethod(joinPoint.getJoinPointHash()),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.CONSTRUCTOR_EXECUTION_INT:
                    ctx = new ExpressionContext(
                            PointcutType.EXECUTION,
                            calleeClassInfo.getConstructor(joinPoint.getJoinPointHash()),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.CONSTRUCTOR_CALL_INT:
                    ctx = new ExpressionContext(
                            PointcutType.CALL,
                            calleeClassInfo.getConstructor(joinPoint.getJoinPointHash()),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.FIELD_SET_INT:
                    ctx = new ExpressionContext(
                            PointcutType.SET,
                            calleeClassInfo.getField(joinPoint.getJoinPointHash()),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.FIELD_GET_INT:
                    ctx = new ExpressionContext(
                            PointcutType.GET,
                            calleeClassInfo.getField(joinPoint.getJoinPointHash()),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.HANDLER_INT:
                    ctx = new ExpressionContext(
                            PointcutType.HANDLER,
                            AsmClassInfo.getClassInfo(joinPoint.getCalleeClassName(), loader),
                            callerMethodInfo
                    );
                    break;
                case JoinPointType.STATIC_INITIALIZATION_INT:
                	ctx = new ExpressionContext(
                	        PointcutType.STATIC_INITIALIZATION,
                	        calleeClassInfo.staticInitializer(),
                	        calleeClassInfo
                	);
            }
            if (expression.getExpression().match(ctx)) {
                matchingJoinPointInfos.add(new MatchingJoinPointInfo(clazz, compilationInfo, ctx));
            }
        }
        return matchingJoinPointInfos;
    }

    /**
     * Returns the emitted join point structure for a specific JIT generated join point class.
     *
     * @param clazz the join point class
     * @return the emitted join point structure
     */
    public static EmittedJoinPoint getEmittedJoinPoint(final Class clazz) {
        return (EmittedJoinPoint) COMPILATION_INFO_REPOSITORY.get(clazz);
    }

    /**
     * Grabs the caller method info.
     *
     * @param callerClassInfo
     * @param emittedJoinPoint
     * @return
     */
    private static MethodInfo getCallerMethodInfo(final ClassInfo callerClassInfo,
                                                  final EmittedJoinPoint emittedJoinPoint) {
        MethodInfo callerMethodInfo = null;
        MethodInfo[] callerMethods = callerClassInfo.getMethods();
        for (int i = 0; i < callerMethods.length; i++) {
            MethodInfo method = callerMethods[i];
            if (method.getName().equals(emittedJoinPoint.getCallerMethodName()) &&
                method.getSignature().equals(emittedJoinPoint.getCallerMethodDesc())) {
                callerMethodInfo = method;
                break;
            }
        }
        return callerMethodInfo;
    }
}
