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

import org.codehaus.aspectwerkz.expression.ast.ASTRoot;
import org.codehaus.aspectwerkz.expression.ast.ASTPointcutReference;
import org.codehaus.aspectwerkz.expression.ast.ASTArgParameter;
import org.codehaus.aspectwerkz.expression.ast.ASTArgs;
import org.codehaus.aspectwerkz.expression.ast.ASTThis;
import org.codehaus.aspectwerkz.expression.ast.ASTTarget;
import org.codehaus.aspectwerkz.expression.ast.Node;
import org.codehaus.aspectwerkz.expression.ast.ASTCflow;
import org.codehaus.aspectwerkz.util.Strings;
import org.codehaus.aspectwerkz.exception.DefinitionException;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.ClassInfoHelper;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.util.ContextClassLoader;

import java.util.Iterator;

import gnu.trove.TIntIntHashMap;
import gnu.trove.TObjectIntHashMap;

/**
 * A visitor to compute the args index of the target (matching) method/constructor which match the advice args. Note:
 * extends the ExpressionVisitor. We should allow for optimization (all=TRUE) by assuming that args(..) does not depends
 * of the matching context. The "(String a, String b):methodX && args(a,b) -OR- methodY && args(b,a)" expression should
 * not be allowed then.
 *
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
 */
public class ArgsIndexVisitor extends ExpressionVisitor {

    /**
     * Classloader used to perform type checks (for target / this bindings)
     * A strong reference is enough since this visitor is not be referenced.
     */
    private ClassLoader m_classLoader;

    /**
     * Update the given context with its runtime information (this, target, args).
     * It should be called for each advice.
     *
     * @param expressionInfo
     * @param context
     */
    public static void updateContextForRuntimeInformation(final ExpressionInfo expressionInfo,
                                                          final ExpressionContext context,
                                                          final ClassLoader loader) {
        ArgsIndexVisitor visitor = new ArgsIndexVisitor(
                expressionInfo, expressionInfo.toString(),
                expressionInfo.getNamespace(),
                expressionInfo.getExpression().getASTRoot(),
                loader
        );
        visitor.match(context);
    }

    private ArgsIndexVisitor(final ExpressionInfo expressionInfo,
                             final String expression,
                             final String namespace,
                             final Node root,
                             final ClassLoader loader) {
        super(expressionInfo, expression, namespace, root);
        m_classLoader = loader;
    }

    //-- overrided methods to compute the args index mapping --//

    public Object visit(ASTPointcutReference node, Object data) {
        // do the sub expression visit
        ExpressionContext context = (ExpressionContext) data;
        ExpressionNamespace namespace = ExpressionNamespace.getNamespace(m_namespace);
        ExpressionInfo expressionInfo = namespace.getExpressionInfo(node.getName());

        ArgsIndexVisitor referenced = new ArgsIndexVisitor(
                expressionInfo, expressionInfo.toString(),
                expressionInfo.getNamespace(),
                expressionInfo.getExpression().getASTRoot(),
                m_classLoader
        );

        // keep track of the state we already had
        String targetSoFar = context.m_targetBoundedName;
        String thisSoFar = context.m_thisBoundedName;
        boolean targetWithRuntimeCheckSoFar = context.m_targetWithRuntimeCheck;
        TObjectIntHashMap exprIndexToTargetIndexSoFar = (TObjectIntHashMap) context.m_exprIndexToTargetIndex.clone();

        context.resetRuntimeState();
        Boolean match = referenced.matchUndeterministic(context);

        // merge the state
        if (context.m_targetBoundedName == null) {
            context.m_targetBoundedName = targetSoFar;
        } else if (targetSoFar != null) {
            if (node.jjtGetNumChildren() == 1) {
                String referenceCallArg = ((ASTArgParameter) node.jjtGetChild(0)).getTypePattern().getPattern();
                if (!targetSoFar.equals(referenceCallArg)) {
                    throw new UnsupportedOperationException("should not occur");
                }
            }
        }
        if (context.m_thisBoundedName == null) {
            context.m_thisBoundedName = thisSoFar;
        } else if (thisSoFar != null) {
            if (node.jjtGetNumChildren() == 1) {
                String referenceCallArg = ((ASTArgParameter) node.jjtGetChild(0)).getTypePattern().getPattern();
                if (!thisSoFar.equals(referenceCallArg)) {
                    throw new UnsupportedOperationException("should not occur");
                }
            }
        }
        if (!context.m_targetWithRuntimeCheck) {
            // restore
            context.m_targetWithRuntimeCheck = targetWithRuntimeCheckSoFar;
        }
        if (context.m_exprIndexToTargetIndex.isEmpty()) {
            // restore
            context.m_exprIndexToTargetIndex = exprIndexToTargetIndexSoFar;
        } else if (!exprIndexToTargetIndexSoFar.isEmpty()) {
            //should merge ?
            throw new UnsupportedOperationException("should not occur");
        }


        // update the this and target bounded name from this last visit as well as args
        TObjectIntHashMap exprToTargetArgIndexes = new TObjectIntHashMap();
        for (int i = 0; i < node.jjtGetNumChildren(); i++) {
            String referenceCallArg = ((ASTArgParameter) node.jjtGetChild(i)).getTypePattern().getPattern();
            String referentArg = expressionInfo.getArgumentNameAtIndex(i);
            if (referentArg.equals(context.m_targetBoundedName)) {
                context.m_targetBoundedName = referenceCallArg;
                assertIsInstanceOf(
                        expressionInfo.getArgumentType(referentArg),
                        m_expressionInfo.getArgumentType(referenceCallArg)
                );
            } else if (referentArg.equals(context.m_thisBoundedName)) {
                context.m_thisBoundedName = referenceCallArg;
                assertIsInstanceOf(
                        expressionInfo.getArgumentType(referentArg),
                        m_expressionInfo.getArgumentType(referenceCallArg)
                );
            } else {
                int adviceArgIndex = i;
                if (context.m_exprIndexToTargetIndex.containsKey(referentArg)) {
                    int targetArgIndex = context.m_exprIndexToTargetIndex.get(referentArg);
                    exprToTargetArgIndexes.put(referenceCallArg, targetArgIndex);
                }

            }
        }
        // merge with index found so far (inlined args() f.e.)
        Object[] soFar = exprIndexToTargetIndexSoFar.keys();
        for (int i = 0; i < soFar.length; i++) {
            String name = (String) soFar[i];
            if (!exprToTargetArgIndexes.containsKey(name)) {
                exprToTargetArgIndexes.put(name, exprIndexToTargetIndexSoFar.get(name));
            }
        }
        context.m_exprIndexToTargetIndex = exprToTargetArgIndexes;
        return match;
    }

    public Object visit(ASTCflow node, Object data) {
        // do the sub expression visit
        ExpressionContext context = (ExpressionContext) data;
        //ExpressionNamespace namespace = ExpressionNamespace.getNamespace(m_namespace);
        //ExpressionInfo expressionInfo = namespace.getExpressionInfo(node.getName());

        ExpressionInfo expressionInfo = new ExpressionInfo(
                node.jjtGetChild(0), m_namespace
        );
        expressionInfo.inheritPossibleArgumentFrom(m_expressionInfo);

        ArgsIndexVisitor referenced = new ArgsIndexVisitor(
                expressionInfo, "N/A",
                m_namespace,
                node.jjtGetChild(0),
                m_classLoader
        );

        // keep track of the state we already had
        String targetSoFar = context.m_targetBoundedName;
        String thisSoFar = context.m_thisBoundedName;
        boolean targetWithRuntimeCheckSoFar = context.m_targetWithRuntimeCheck;
        TObjectIntHashMap exprIndexToTargetIndexSoFar = (TObjectIntHashMap) context.m_exprIndexToTargetIndex.clone();

        context.resetRuntimeState();
        Boolean match = referenced.matchUndeterministic(context);

        // TODO FIX ME merge the state
        if (context.m_targetBoundedName == null) {
            context.m_targetBoundedName = targetSoFar;
        } else if (targetSoFar != null) {
            // cflow target
        }
        if (context.m_thisBoundedName == null) {
            context.m_thisBoundedName = thisSoFar;
        } else if (thisSoFar != null) {
            // cflow this
        }
        if (!context.m_targetWithRuntimeCheck) {
            // restore
            context.m_targetWithRuntimeCheck = targetWithRuntimeCheckSoFar;
        }
        if (context.m_exprIndexToTargetIndex.isEmpty()) {
            // restore
            context.m_exprIndexToTargetIndex = exprIndexToTargetIndexSoFar;
        } else if (!exprIndexToTargetIndexSoFar.isEmpty()) {
            //should merge ?
            for (int i = 0; i < exprIndexToTargetIndexSoFar.keys().length; i++) {
                Object o = exprIndexToTargetIndexSoFar.keys()[i];
                context.m_exprIndexToTargetIndex.put(o, exprIndexToTargetIndexSoFar.get(o));
            }
        }
        return match;
    }

    public Object visit(ASTArgs node, Object data) {
        return super.visit(node, data);
    }

    public Object visit(ASTArgParameter node, Object data) {
        // do the visit
        Boolean match = (Boolean) super.visit(node, data);

        // get the pointcut signature arg index of the arg we are visiting
        int pointcutArgIndex = -1;
        if (node.getTypePattern().getPattern().indexOf(".") < 0) {
            pointcutArgIndex = m_expressionInfo.getArgumentIndex(node.getTypePattern().getPattern());
        }

        // if match and we are visiting a parameter binding (not a type matching)
        if (pointcutArgIndex >= 0 && Boolean.TRUE.equals(match)) {
            ExpressionContext ctx = (ExpressionContext) data;
            ctx.m_exprIndexToTargetIndex.put(
                    m_expressionInfo.getArgumentNameAtIndex(pointcutArgIndex), ctx.getCurrentTargetArgsIndex()
            );
        }
        return match;
    }

    public Object visit(ASTThis node, Object data) {
        // if the this(..) node identifier appears in the pointcut signature, we have a bounded type
        if (m_expressionInfo.getArgumentType(node.getIdentifier()) != null) {
            ExpressionContext ctx = (ExpressionContext) data;
            if (ctx.m_thisBoundedName == null) {
                ctx.m_thisBoundedName = node.getIdentifier();
            } else if (ctx.m_thisBoundedName != node.getIdentifier()) {
                throw new DefinitionException(
                        "this(..) seems to be bounded to different bounded entities in \""
                        + m_expressionInfo.toString() + "\" in " +
                        m_expressionInfo.getNamespace()
                        + " : found " + ctx.m_targetBoundedName + " and " +
                        node.getIdentifier()
                );
            }
        }
        return super.visit(node, data);
    }

    public Object visit(ASTTarget node, Object data) {
        // if the target(..) node identifier appears in the pointcut signature, we have a bounded type
        if (m_expressionInfo.getArgumentType(node.getIdentifier()) != null) {
            ExpressionContext ctx = (ExpressionContext) data;
            if (ctx.m_targetBoundedName == null) {
                ctx.m_targetBoundedName = node.getIdentifier();
            } else if (ctx.m_targetBoundedName != node.getIdentifier()) {
                throw new DefinitionException(
                        "target(..) seems to be bounded to different bounded entities in \""
                        + m_expressionInfo.toString() + "\" in " +
                        m_expressionInfo.getNamespace()
                        + " : found " + ctx.m_targetBoundedName + " and " +
                        node.getIdentifier()
                );
            }
        }
        // keep track if the result was undetermined: we will need a runtime check
        Object match = super.visit(node, data);
        if (match == null) {
            ((ExpressionContext) data).m_targetWithRuntimeCheck = true;
        }
        return match;
    }

    /**
     * Ensure className is an instance of superClass name (both super class / interface just like "instanceof")
     * Or throw an exception
     *
     * @param className
     * @param superClassName
     */
    private void assertIsInstanceOf(String className, String superClassName) {
        if (className.equals(superClassName)) {
            ;//fine
        } else {
            // advice(Foo f) for pc(f) with pc(Object o) for example
            // we need to ensure that Foo is an instance of Object
            ClassInfo classInfo = AsmClassInfo.getClassInfo(className, m_classLoader);
            boolean instanceOf = ClassInfoHelper.instanceOf(classInfo, superClassName);
            if (!instanceOf) {
                throw new DefinitionException(
                        "Attempt to reference a pointcut with incompatible object type: for \""
                        + m_expression + "\" , " + className + " is not an instance of " +
                        superClassName +
                        "."
                        + " Error occured in " + m_namespace
                );
            }
        }
    }
}