/**************************************************************************************
 * 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 QPL license       *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.joinpoint;

import java.lang.reflect.Method;
import java.util.StringTokenizer;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.io.ObjectInputStream;

import org.apache.bcel.generic.Type;

import org.codehaus.aspectwerkz.pointcut.CallPointcut;
import org.codehaus.aspectwerkz.metadata.MethodMetaData;
import org.codehaus.aspectwerkz.metadata.ReflectionMetaDataMaker;
import org.codehaus.aspectwerkz.metadata.ClassMetaData;
import org.codehaus.aspectwerkz.transform.TransformationUtil;
import org.codehaus.aspectwerkz.IndexTuple;
import org.codehaus.aspectwerkz.SystemLoader;
import org.codehaus.aspectwerkz.System;
import org.codehaus.aspectwerkz.xmldef.XmlDefSystem;
import org.codehaus.aspectwerkz.attribdef.AttribDefSystem;

/**
 * Matches well defined point of execution in the program where a method is
 * invoked. Stores meta data from the join point. I.e. a reference to
 * original object and method, name and type of the field etc. Handles the
 * invocation of the advices added to the join point.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér</a>
 */
public class CallerSideJoinPoint extends AbstractJoinPoint {

    /**
     * The AspectWerkz system for this join point.
     */
    private transient System m_system;

    /**
     * The serial version uid for the class.
     *
     * @todo recalculate
     */
    private static final long serialVersionUID = -8831127199517513612L;;

    /**
     * and reference to the caller class.
     */
    protected Class m_targetClass;

    /**
     * The caller method.
     */
    protected Method m_callerMethod;

    /**
     * The name of the callee method.
     */
    protected String m_calleeMethodName;

    /**
     * The name of the callee class.
     */
    protected String m_calleeClassName;

    /**
     * The signature for the callee method.
     */
    protected String m_calleeMethodSignature;

    /**
     * The callee method parameter types.
     */
    protected Class[] m_calleeMethodParameterTypes = null;

    /**
     * The callee method parameter types names.
     */
    protected String[] m_calleeMethodParameterTypeNames = null;

    /**
     * The callee method return type.
     */
    protected Class m_calleeMethodReturnType = null;

    /**
     * The callee method return type name.
     */
    protected String m_calleeMethodReturnTypeName = null;

    /**
     * The name of the caller method.
     */
    protected String m_callerMethodName;

    /**
     * The signature for the caller method.
     */
    protected String m_callerMethodSignature;

    /**
     * The caller method parameter types.
     */
    protected Class[] m_callerMethodParameterTypes = null;

    /**
     * The caller method parameter types names.
     */
    protected String[] m_callerMethodParameterTypeNames = null;

    /**
     * The caller method return type.
     */
    protected Class m_callerMethodReturnType = null;

    /**
     * The caller method return type name.
     */
    protected String m_callerMethodReturnTypeName = null;

    /**
     * The pre advices applied to the join point.
     */
    protected IndexTuple[] m_preAdvices = new IndexTuple[0];

    /**
     * The post advices applied to the join point.
     */
    protected IndexTuple[] m_postAdvices = new IndexTuple[0];

    /**
     * Marks the join point as initialized.
     */
    protected boolean m_initialized = false;

    /**
     * Meta-data for the class.
     */
    protected ClassMetaData m_classMetaData;

    /**
     * Meta-data for the method.
     */
    protected MethodMetaData m_methodMetaData;

    /**
     * The UUID for the AspectWerkz system to use.
     */
    protected String m_uuid;

    /**
     * Creates a new CallerSideJoinPoint object.
     *
     * @param uuid the UUID for the AspectWerkz system to use
     * @param callerClass the caller class
     * @param callerMethodName the full caller method name (including the class name)
     * @param callerMethodSignature the caller method signature
     * @param calleeMethodName the full callee method name (including the class name)
     * @param calleeMethodSignature the callee method signature
     */
    public CallerSideJoinPoint(final String uuid,
                               final Class callerClass,
                               final String callerMethodName,
                               final String callerMethodSignature,
                               final String calleeMethodName,
                               final String calleeMethodSignature) {
        if (uuid == null) throw new IllegalArgumentException("uuid can not be null");
        if (callerClass == null) throw new IllegalArgumentException("original class can not be null");
        if (callerMethodName == null) throw new IllegalArgumentException("caller method name can not be null");
        if (callerMethodSignature == null) throw new IllegalArgumentException("caller signature can not be null");
        if (calleeMethodName == null) throw new IllegalArgumentException("callee method name can not be null");
        if (calleeMethodSignature == null) throw new IllegalArgumentException("callee signature can not be null");

        m_system = SystemLoader.getSystem(uuid);
        m_system.initialize();

        m_uuid = uuid;
        m_targetClass = callerClass;
        m_callerMethodName = callerMethodName;
        m_callerMethodSignature = callerMethodSignature;

        StringTokenizer tokenizer = new StringTokenizer(
                calleeMethodName,
                TransformationUtil.CALL_SIDE_DELIMITER
        );
        m_calleeClassName = tokenizer.nextToken();
        m_calleeMethodName = tokenizer.nextToken();
        m_calleeMethodSignature = calleeMethodSignature;

        createMetaData();
    }

    /**
     * To be called instead of proceed() when a new thread is spawned.
     * Otherwise the result is unpredicable.
     *
     * @return the result from the next invocation
     * @throws Throwable
     */
    public Object proceedInNewThread() throws Throwable {
        return null;
    }

    /**
     * Invokes the next advice in the chain and when it reaches the end
     * of the chain the original method.
     *
     * @return the result from the next invocation
     * @throws Throwable
     */
    public Object proceed() throws Throwable {
        return null;
    }

    /**
     * Invokes the next pre advice in the chain until it reaches the end.
     */
    public void pre() throws Throwable {
        if (!m_initialized) {
            loadAdvices();
        }
        // TODO: handle a list of systems that nees to be matched to the advice lists (loadAdvices etc.)
        if (m_system.isAttribDef()) {
            AttribDefSystem system = (AttribDefSystem)m_system;
            for (int i = 0, j = m_preAdvices.length; i < j; i++) {
                try {
                    IndexTuple index = m_preAdvices[i];
                    int aspectIndex = index.getAspectIndex();
                    int methodIndex = index.getMethodIndex();
                    system.getAspect(aspectIndex).___AW_invokeAdvice(methodIndex, this);
                }
                catch (ArrayIndexOutOfBoundsException ex) {
                    throw new RuntimeException(createAdvicesNotCorrectlyMappedMessage());
                }
            }
        }
        else {
            XmlDefSystem system = (XmlDefSystem)m_system;
            for (int i = 0, j = m_preAdvices.length; i < j; i++) {
                try {
                    system.getAdvice(m_preAdvices[i]).doExecute(this);
                }
                catch (ArrayIndexOutOfBoundsException ex) {
                    throw new RuntimeException(createAdvicesNotCorrectlyMappedMessage());
                }
            }
        }
    }

    /**
     * Invokes the next post advice in the chain until it reaches the end.
     */
    public void post() throws Throwable {
        if (!m_initialized) {
            loadAdvices();
        }
        // TODO: handle a list of systems that nees to be matched to the advice lists (loadAdvices etc.)
        if (m_system.isAttribDef()) {
            AttribDefSystem system = (AttribDefSystem)m_system;
            for (int i = m_postAdvices.length - 1; i >= 0; i--) {
                try {
                    IndexTuple index = m_postAdvices[i];
                    int aspectIndex = index.getAspectIndex();
                    int methodIndex = index.getMethodIndex();
                    system.getAspect(aspectIndex).___AW_invokeAdvice(methodIndex, this);
                }
                catch (ArrayIndexOutOfBoundsException ex) {
                    throw new RuntimeException(createAdvicesNotCorrectlyMappedMessage());
                }
            }
        }
        else {
            XmlDefSystem system = (XmlDefSystem)m_system;
            for (int i = m_postAdvices.length - 1; i >= 0; i--) {
                try {
                    system.getAdvice(m_postAdvices[i]).doExecute(this);
                }
                catch (ArrayIndexOutOfBoundsException ex) {
                    throw new RuntimeException(createAdvicesNotCorrectlyMappedMessage());
                }
            }
        }
    }

    /**
     * Returns the target instance.
     *
     * @return the target instance
     */
    public Object getTargetInstance() {
        return null;
    }

    /**
     * Returns the target class.
     * Same as getCallerClass().
     *
     * @return the target class
     */
    public Class getTargetClass() {
        return m_targetClass;
    }

    /**
     * Returns the callee method name.
     *
     * @return the callee method name
     */
    public String getCalleeMethodName() {
        return m_calleeMethodName;
    }

    /**
     * Returns the name of callee class.
     *
     * @return the callee class name
     */
    public String getCalleeClassName() {
        return m_calleeClassName;
    }

    /**
     * Returns the parameter types for the callee method.
     *
     * @return the parameter types
     */
    public Class[] getCalleeMethodParameterTypes() {
        if (m_calleeMethodParameterTypes == null) {
            Type[] parameterTypes = Type.getArgumentTypes(m_calleeMethodSignature);
            m_calleeMethodParameterTypes = new Class[parameterTypes.length];
            for (int i = 0; i < parameterTypes.length; i++) {
                m_calleeMethodParameterTypes[i] =
                        TransformationUtil.convertBcelTypeToClass(parameterTypes[i]);
            }
        }
        return m_calleeMethodParameterTypes;
    }

    /**
     * Returns the parameter type names for the callee method.
     *
     * @return the parameter type names
     */
    public String[] getCalleeMethodParameterTypeNames() {
        if (m_calleeMethodParameterTypeNames == null) {
            Type[] parameterTypes = Type.getArgumentTypes(m_calleeMethodSignature);
            m_calleeMethodParameterTypeNames = new String[parameterTypes.length];
            for (int i = 0; i < parameterTypes.length; i++) {
                m_calleeMethodParameterTypeNames[i] = parameterTypes[i].toString();
            }
        }
        return m_calleeMethodParameterTypeNames;
    }

    /**
     * Returns the return type for the callee method.
     *
     * @return the return type
     */
    public Class getCalleeMethodReturnType() {
        if (m_calleeMethodReturnType == null) {
            Type returnType = Type.getReturnType(m_calleeMethodSignature);
            m_calleeMethodReturnType = TransformationUtil.convertBcelTypeToClass(returnType);
        }
        return m_calleeMethodReturnType;
    }

    /**
     * Returns the return type name for the callee method.
     *
     * @return the return type name
     */
    public String getCalleeMethodReturnTypeName() {
        if (m_calleeMethodReturnTypeName == null) {
            m_calleeMethodReturnTypeName = Type.getReturnType(m_calleeMethodSignature).toString();
        }
        return m_calleeMethodReturnTypeName;
    }

    /**
     * Returns the callee method signature.
     *
     * @return the callee method signature
     */
    public String getCalleeMethodSignature() {
        return m_calleeMethodSignature;
    }

    /**
     * Returns the caller method name.
     *
     * @return the caller method name
     */
    public String getCallerMethodName() {
        return m_callerMethodName;
    }

    /**
     * Returns the name of caller class.
     *
     * @return the caller class name
     */
    public String getCallerClassName() {
        return m_targetClass.getName();
    }

    /**
     * Returns the caller class.
     * Same as getTargetClass().
     *
     * @return the caller class
     */
    public Class getCallerClass() {
        return m_targetClass;
    }

    /**
     * Returns the parameter types for the method.
     *
     * @return the parameter types
     */
    public Class[] getCallerMethodParameterTypes() {
        if (m_callerMethodParameterTypes == null) {
            Type[] parameterTypes = Type.getArgumentTypes(m_callerMethodSignature);
            m_callerMethodParameterTypes = new Class[parameterTypes.length];
            for (int i = 0; i < parameterTypes.length; i++) {
                m_callerMethodParameterTypes[i] =
                        TransformationUtil.convertBcelTypeToClass(parameterTypes[i]);
            }
        }
        return m_callerMethodParameterTypes;
    }

    /**
     * Returns the parameter type names for the caller method.
     *
     * @return the parameter type names
     */
    public String[] getCallerMethodParameterTypeNames() {
        if (m_callerMethodParameterTypeNames == null) {
            Type[] parameterTypes = Type.getArgumentTypes(m_callerMethodSignature);
            m_callerMethodParameterTypeNames = new String[parameterTypes.length];
            for (int i = 0; i < parameterTypes.length; i++) {
                m_callerMethodParameterTypeNames[i] = parameterTypes[i].toString();
            }
        }
        return m_callerMethodParameterTypeNames;
    }

    /**
     * Returns the return type for the caller method.
     *
     * @return the return type
     */
    public Class getCallerMethodReturnType() {
        if (m_callerMethodReturnType == null) {
            Type returnType = Type.getReturnType(m_callerMethodSignature);
            m_callerMethodReturnType = TransformationUtil.convertBcelTypeToClass(returnType);
        }
        return m_callerMethodReturnType;
    }

    /**
     * Returns the return type name for the caller method.
     *
     * @return the return type name
     */
    public String getCallerMethodReturnTypeName() {
        if (m_callerMethodReturnTypeName == null) {
            m_callerMethodReturnTypeName = Type.getReturnType(m_callerMethodSignature).toString();
        }
        return m_callerMethodReturnTypeName;
    }

    /**
     * Returns the caller method signature.
     *
     * @return the caller method signature
     */
    public String getCallerMethodSignature() {
        return m_callerMethodSignature;
    }

    /**
     * Loads the advices for this pointcut.
     */
    private void loadAdvices() {
        synchronized (m_preAdvices) {
            synchronized (m_postAdvices) {
                List preAdvices = new ArrayList();
                List postAdvices = new ArrayList();

                ClassMetaData classMetaData = new ClassMetaData();
                classMetaData.setName(getCalleeClassName());
                List pointcuts = m_system.getCallPointcuts(
                        classMetaData,
                        m_methodMetaData
                );

                for (Iterator it = pointcuts.iterator(); it.hasNext();) {
                    CallPointcut callerSidePointcut = (CallPointcut)it.next();

                    IndexTuple[] preAdviceIndexes = callerSidePointcut.getPreAdviceIndexes();
                    for (int j = 0; j < preAdviceIndexes.length; j++) {
                        preAdvices.add(preAdviceIndexes[j]);
                    }

                    IndexTuple[] postAdviceIndexes = callerSidePointcut.getPostAdviceIndexes();
                    for (int j = 0; j < postAdviceIndexes.length; j++) {
                        postAdvices.add(postAdviceIndexes[j]);
                    }
                }

                m_preAdvices = new IndexTuple[preAdvices.size()];
                int i = 0;
                for (Iterator it = preAdvices.iterator(); it.hasNext(); i++) {
                    m_preAdvices[i] = (IndexTuple)it.next();
                }
                m_postAdvices = new IndexTuple[postAdvices.size()];
                i = 0;
                for (Iterator it = postAdvices.iterator(); it.hasNext(); i++) {
                    m_postAdvices[i] = (IndexTuple)it.next();
                }

                m_initialized = true;
            }
        }
    }

    /**
     * Creates meta-data for the join point.
     */
    protected void createMetaData() {
        m_classMetaData = ReflectionMetaDataMaker.createClassMetaData(getTargetClass());
        m_methodMetaData = ReflectionMetaDataMaker.createMethodMetaData(
                getCalleeMethodName(),
                getCalleeMethodParameterTypes(),
                getCalleeMethodReturnType()
        );
    }

    /**
     * Creates an advices not correctly mapped message.
     *
     * @return the message
     */
    protected String createAdvicesNotCorrectlyMappedMessage() {
        StringBuffer cause = new StringBuffer();
        cause.append("advices for ");
        cause.append(getCalleeClassName());
        cause.append("#");
        cause.append(getCalleeMethodName());
        cause.append(" are not correctly mapped");
        return cause.toString();
    }

    /**
     * Provides custom deserialization.
     *
     * @param stream the object input stream containing the serialized object
     * @throws java.lang.Exception in case of failure
     */
    private void readObject(final ObjectInputStream stream) throws Exception {
        ObjectInputStream.GetField fields = stream.readFields();
        m_uuid = (String)fields.get("m_uuid", null);
        m_targetClass = (Class)fields.get("m_targetClass", null);
        m_callerMethod = (Method)fields.get("m_callerMethod", null);
        m_callerMethodName = (String)fields.get("m_callerMethodName", null);
        m_callerMethodSignature = (String)fields.get("m_callerMethodSignature", null);
        m_callerMethodParameterTypes = (Class[])fields.get("m_callerMethodParameterTypes", null);
        m_callerMethodParameterTypeNames = (String[])fields.get("m_callerMethodParameterTypeNames", null);
        m_callerMethodReturnType = (Class)fields.get("m_callerMethodReturnType", null);
        m_callerMethodReturnTypeName = (String)fields.get("m_callerMethodReturnTypeName", null);
        m_calleeMethodName = (String)fields.get("m_calleeMethodName", null);
        m_calleeMethodSignature = (String)fields.get("m_calleeMethodSignature", null);
        m_calleeClassName = (String)fields.get("m_calleeClassName", null);
        m_calleeMethodParameterTypes = (Class[])fields.get("m_calleeMethodParameterTypes", null);
        m_calleeMethodParameterTypeNames = (String[])fields.get("m_calleeMethodParameterTypeNames", null);
        m_calleeMethodReturnType = (Class)fields.get("m_calleeMethodReturnType", null);
        m_calleeMethodReturnTypeName = (String)fields.get("m_calleeMethodReturnTypeName", null);
        m_preAdvices = (IndexTuple[])fields.get("m_preAdvices", null);
        m_postAdvices = (IndexTuple[])fields.get("m_postAdvices", null);
        m_classMetaData = (ClassMetaData)fields.get("m_classMetaData", null);
        m_methodMetaData = (MethodMetaData)fields.get("m_fieldMetaData", null);
        m_initialized = fields.get("m_initialized", false);
        m_system = SystemLoader.getSystem(m_uuid);
        m_system.initialize();
    }
}
