/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.filter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import jakarta.jms.JMSException;

/**
 * A MultiExpressionEvaluator is used to evaluate multiple expressions in single
 * method call. <p/> Multiple Expression/ExpressionListener pairs can be added
 * to a MultiExpressionEvaluator object. When the MultiExpressionEvaluator
 * object is evaluated, all the registed Expressions are evaluated and then the
 * associated ExpressionListener is invoked to inform it of the evaluation
 * result. <p/> By evaluating multiple expressions at one time, some
 * optimizations can be made to reduce the number of computations normally
 * required to evaluate all the expressions. <p/> When this class adds an
 * Expression it wrapps each node in the Expression's AST with a CacheExpression
 * object. Then each CacheExpression object (one for each node) is placed in the
 * cachedExpressions map. The cachedExpressions map allows us to find the sub
 * expressions that are common across two different expressions. When adding an
 * Expression in, if a sub Expression of the Expression is allready in the
 * cachedExpressions map, then instead of wrapping the sub expression in a new
 * CacheExpression object, we reuse the CacheExpression allready int the map.
 * <p/> To help illustrate what going on, lets try to give an exmample: If we
 * denote the AST of a Expression as follows:
 * [AST-Node-Type,Left-Node,Right-Node], then A expression like: "3*5+6" would
 * result in "[*,3,[+,5,6]]" <p/> If the [*,3,[+,5,6]] expression is added to
 * the MultiExpressionEvaluator, it would really be converted to:
 * [c0,[*,3,[c1,[+,5,6]]]] where c0 and c1 represent the CacheExpression
 * expression objects that cache the results of the * and the + operation.
 * Constants and Property nodes are not cached. <p/> If later on we add the
 * following expression [=,11,[+,5,6]] ("11=5+6") to the
 * MultiExpressionEvaluator it would be converted to: [c2,[=,11,[c1,[+,5,6]]]],
 * where c2 is a new CacheExpression object but c1 is the same CacheExpression
 * used in the previous expression. <p/> When the expressions are evaluated, the
 * c1 CacheExpression object will only evaluate the [+,5,6] expression once and
 * cache the resulting value. Hence evauating the second expression costs less
 * because that [+,5,6] is not done 2 times. <p/> Problems: - cacheing the
 * values introduces overhead. It may be possible to be smarter about WHICH
 * nodes in the AST are cached and which are not. - Current implementation is
 * not thread safe. This is because you need a way to invalidate all the cached
 * values so that the next evaluation re-evaluates the nodes. By going single
 * threaded, chache invalidation is done quickly by incrementing a 'view'
 * counter. When a CacheExpressionnotices it's last cached value was generated
 * in an old 'view', it invalidates its cached value.
 * 
 *  $Date: 2005/08/27 03:52:36 $
 */
public class MultiExpressionEvaluator {

    Map<String, ExpressionListenerSet> rootExpressions = new HashMap<String, ExpressionListenerSet>();
    Map<Expression, CacheExpression> cachedExpressions = new HashMap<Expression, CacheExpression>();

    int view;

    /**
     * A UnaryExpression that caches the result of the nested expression. The
     * cached value is valid if the
     * CacheExpression.cview==MultiExpressionEvaluator.view
     */
    public class CacheExpression extends UnaryExpression {
        short refCount;
        int cview = view - 1;
        Object cachedValue;
        int cachedHashCode;

        public CacheExpression(Expression realExpression) {
            super(realExpression);
            cachedHashCode = realExpression.hashCode();
        }

        /**
         * @see org.apache.activemq.filter.Expression#evaluate(MessageEvaluationContext)
         */
        public Object evaluate(MessageEvaluationContext message) throws JMSException {
            if (view == cview) {
                return cachedValue;
            }
            cachedValue = right.evaluate(message);
            cview = view;
            return cachedValue;
        }

        public int hashCode() {
            return cachedHashCode;
        }

        public boolean equals(Object o) {
            if (o == null) {
                return false;
            }
            return ((CacheExpression)o).right.equals(right);
        }

        public String getExpressionSymbol() {
            return null;
        }

        public String toString() {
            return right.toString();
        }

    }

    /**
     * Multiple listeners my be interested in the results of a single
     * expression.
     */
    static class ExpressionListenerSet {
        Expression expression;
        List<ExpressionListener> listeners = new ArrayList<ExpressionListener>();
    }

    /**
     * Objects that are interested in the results of an expression should
     * implement this interface.
     */
    static interface ExpressionListener {
        void evaluateResultEvent(Expression selector, MessageEvaluationContext message, Object result);
    }

    /**
     * Adds an ExpressionListener to a given expression. When evaluate is
     * called, the ExpressionListener will be provided the results of the
     * Expression applied to the evaluated message.
     */
    public void addExpressionListner(Expression selector, ExpressionListener c) {
        ExpressionListenerSet data = rootExpressions.get(selector.toString());
        if (data == null) {
            data = new ExpressionListenerSet();
            data.expression = addToCache(selector);
            rootExpressions.put(selector.toString(), data);
        }
        data.listeners.add(c);
    }

    /**
     * Removes an ExpressionListener from receiving the results of a given
     * evaluation.
     */
    public boolean removeEventListner(String selector, ExpressionListener c) {
        String expKey = selector;
        ExpressionListenerSet d = rootExpressions.get(expKey);
        // that selector had not been added.
        if (d == null) {
            return false;
        }
        // that selector did not have that listeners..
        if (!d.listeners.remove(c)) {
            return false;
        }

        // If there are no more listeners for this expression....
        if (d.listeners.size() == 0) {
            // Un-cache it...
            removeFromCache((CacheExpression)d.expression);
            rootExpressions.remove(expKey);
        }
        return true;
    }

    /**
     * Finds the CacheExpression that has been associated with an expression. If
     * it is the first time the Expression is being added to the Cache, a new
     * CacheExpression is created and associated with the expression. <p/> This
     * method updates the reference counters on the CacheExpression to know when
     * it is no longer needed.
     */
    private CacheExpression addToCache(Expression expr) {

        CacheExpression n = cachedExpressions.get(expr);
        if (n == null) {
            n = new CacheExpression(expr);
            cachedExpressions.put(expr, n);
            if (expr instanceof UnaryExpression) {

                // Cache the sub expressions too
                UnaryExpression un = (UnaryExpression)expr;
                un.setRight(addToCache(un.getRight()));

            } else if (expr instanceof BinaryExpression) {

                // Cache the sub expressions too.
                BinaryExpression bn = (BinaryExpression)expr;
                bn.setRight(addToCache(bn.getRight()));
                bn.setLeft(addToCache(bn.getLeft()));

            }
        }
        n.refCount++;
        return n;
    }

    /**
     * Removes an expression from the cache. Updates the reference counters on
     * the CacheExpression object. When the refernce counter goes to zero, the
     * entry int the Expression to CacheExpression map is removed.
     * 
     * @param cn
     */
    private void removeFromCache(CacheExpression cn) {
        cn.refCount--;
        Expression realExpr = cn.getRight();
        if (cn.refCount == 0) {
            cachedExpressions.remove(realExpr);
        }
        if (realExpr instanceof UnaryExpression) {
            UnaryExpression un = (UnaryExpression)realExpr;
            removeFromCache((CacheExpression)un.getRight());
        }
        if (realExpr instanceof BinaryExpression) {
            BinaryExpression bn = (BinaryExpression)realExpr;
            removeFromCache((CacheExpression)bn.getRight());
        }
    }

    /**
     * Evaluates the message against all the Expressions added to this object.
     * The added ExpressionListeners are notified of the result of the
     * evaluation.
     * 
     * @param message
     */
    public void evaluate(MessageEvaluationContext message) {
        Collection<ExpressionListenerSet> expressionListeners = rootExpressions.values();
        for (Iterator<ExpressionListenerSet> iter = expressionListeners.iterator(); iter.hasNext();) {
            ExpressionListenerSet els = iter.next();
            try {
                Object result = els.expression.evaluate(message);
                for (Iterator<ExpressionListener> iterator = els.listeners.iterator(); iterator.hasNext();) {
                    ExpressionListener l = iterator.next();
                    l.evaluateResultEvent(els.expression, message, result);
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
}
